From 0a12da74dfd2859c849b7cb5973f9af7489df51f Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Tue, 12 May 2026 08:42:13 +0200 Subject: [PATCH 01/17] test(replayer): unit tests for replayer 1. Adds a test suite for the replayer component, distributed in three files: * **replay_test.cpp**: Validates the ReplayImpl logic, including time advancement, play/pause/stop state transitions, seeking within indexed archives, and the correct sequential application of keyframes and payloads over time. * **replayed_object_test.cpp**: Verifies the correct restoration of ReplayedObjects from snapshots, ensuring injected changes are only reflected after flush/commit. It guarantees immutability by throwing exceptions on mutable operations and validates proper serialization and property getter resolution. * **replayer_test.cpp**: Tests the ReplayerImpl orchestration, confirming robust error handling when opening invalid or duplicate archives, and verifying proper session management when closing active recordings. 2. It centralizes test utilities by migrating them to archive_test_helpers.h, establishing common functions for temporary directory management, timestamps, and archive setup. 3. Resolves some bugfixes in: * **components/replayer/replay.cpp**: seeking to an intermediate time, only loaded the closest keyframe, ignoring subsequent events. Now calculates the time difference and processes incremental events up to the exact taget time. * **libs/db/input.cpp**: handle an empty keyframe & handle the out of range search resolves SEN-1479 --- CMakeLists.txt | 1 + components/recorder/test/CMakeLists.txt | 3 +- .../recorder/test/recorder_crash_test.cpp | 46 +- components/replayer/CMakeLists.txt | 6 + components/replayer/src/replay.cpp | 5 + components/replayer/test/CMakeLists.txt | 37 + components/replayer/test/replay_test.cpp | 456 +++++++++++ .../replayer/test/replayed_object_test.cpp | 711 ++++++++++++++++++ components/replayer/test/replayer_test.cpp | 218 ++++++ .../replayer/test/replayer_test_helpers.h | 41 + .../replayer/test/stl/replay_test_class.stl | 59 ++ libs/db/src/input.cpp | 10 + libs/db/test/CMakeLists.txt | 1 + libs/db/test/creation_test.cpp | 30 +- libs/db/test/db_test_helpers.h | 47 +- libs/db/test/input_test.cpp | 18 +- test/support/CMakeLists.txt | 28 + test/support/src/archive_test_helpers.cpp | 9 + test/support/src/archive_test_helpers.h | 133 ++++ 19 files changed, 1754 insertions(+), 105 deletions(-) create mode 100644 components/replayer/test/CMakeLists.txt create mode 100644 components/replayer/test/replay_test.cpp create mode 100644 components/replayer/test/replayed_object_test.cpp create mode 100644 components/replayer/test/replayer_test.cpp create mode 100644 components/replayer/test/replayer_test_helpers.h create mode 100644 components/replayer/test/stl/replay_test_class.stl create mode 100644 test/support/CMakeLists.txt create mode 100644 test/support/src/archive_test_helpers.cpp create mode 100644 test/support/src/archive_test_helpers.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 14b41f34..ea123f33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,7 @@ if(SEN_BUILD_TESTS) add_subdirectory(test/util/calls_on_removal) add_subdirectory(test/util/publish_types_manually) add_subdirectory(test/util/query_test) + add_subdirectory(test/support) endif() # coverage diff --git a/components/recorder/test/CMakeLists.txt b/components/recorder/test/CMakeLists.txt index a31f1f56..a9fd1f75 100644 --- a/components/recorder/test/CMakeLists.txt +++ b/components/recorder/test/CMakeLists.txt @@ -5,7 +5,7 @@ # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== -set(recorder_tests recorder_crash_test.cpp recorder_test.cpp) +set(recorder_tests recorder_crash_test.cpp) add_sen_unit_test_suite( recorder_test @@ -16,6 +16,7 @@ add_sen_unit_test_suite( sen::db recorder_for_testing spdlog::spdlog + archive_test_helpers ) sen_generate_cpp( diff --git a/components/recorder/test/recorder_crash_test.cpp b/components/recorder/test/recorder_crash_test.cpp index 80c4df55..f095d694 100644 --- a/components/recorder/test/recorder_crash_test.cpp +++ b/components/recorder/test/recorder_crash_test.cpp @@ -5,6 +5,9 @@ // © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. // ===================================================================================================================== +// shared test helpers +#include "archive_test_helpers.h" + // sen #include "sen/core/base/numbers.h" #include "sen/core/meta/property.h" @@ -20,7 +23,6 @@ // std #include -#include #include #include #include @@ -33,20 +35,6 @@ namespace class RecorderCrashResilienceTest: public ::testing::Test { protected: - void SetUp() override - { - testDir_ = std::filesystem::temp_directory_path() / ("recorder_test_" + std::to_string(std::time(nullptr))); - std::filesystem::create_directories(testDir_); - } - - void TearDown() override - { - if (std::filesystem::exists(testDir_)) - { - std::filesystem::remove_all(testDir_); - } - } - static void truncateFile(const std::filesystem::path& path, std::uintmax_t newSize) { std::filesystem::resize_file(path, newSize); @@ -54,10 +42,10 @@ class RecorderCrashResilienceTest: public ::testing::Test static std::uintmax_t getFileSize(const std::filesystem::path& path) { return std::filesystem::file_size(path); } - [[nodiscard]] const std::filesystem::path& getTestDir() const { return testDir_; } + [[nodiscard]] const std::filesystem::path& getTestDir() const { return tempDir_.path(); } private: - std::filesystem::path testDir_; + sen::test::TempDir tempDir_ {"recorder_test_"}; }; /// @test @@ -66,12 +54,8 @@ TEST_F(RecorderCrashResilienceTest, RecordingCreatesValidArchive) { auto kernel = sen::kernel::TestKernel::fromYamlString(""); - sen::db::OutSettings settings; - settings.name = "test_recording"; - settings.folder = getTestDir().string(); - settings.indexKeyframes = true; - - const auto archivePath = getTestDir() / settings.name; + auto settings = sen::test::makeArchiveSettings("test_recording", getTestDir()); + const auto archivePath = sen::test::makeArchivePath("test_recording", getTestDir()); { sen::db::Output output(std::move(settings), []() {}); @@ -95,12 +79,8 @@ TEST_F(RecorderCrashResilienceTest, TruncatedRecording_CanStillBeOpened) { auto kernel = sen::kernel::TestKernel::fromYamlString(""); - sen::db::OutSettings settings; - settings.name = "test_recording"; - settings.folder = getTestDir().string(); - settings.indexKeyframes = true; - - const auto archivePath = getTestDir() / settings.name; + auto settings = sen::test::makeArchiveSettings("test_recording", getTestDir()); + const auto archivePath = sen::test::makeArchivePath("test_recording", getTestDir()); const auto runtimePath = archivePath / "runtime"; { @@ -137,12 +117,8 @@ TEST_F(RecorderCrashResilienceTest, TypesFileCreatedWithArchive) { auto kernel = sen::kernel::TestKernel::fromYamlString(""); - sen::db::OutSettings settings; - settings.name = "test_recording"; - settings.folder = getTestDir().string(); - settings.indexKeyframes = true; - - const auto archivePath = getTestDir() / settings.name; + auto settings = sen::test::makeArchiveSettings("test_recording", getTestDir()); + const auto archivePath = sen::test::makeArchivePath("test_recording", getTestDir()); { sen::db::Output output(std::move(settings), []() {}); diff --git a/components/replayer/CMakeLists.txt b/components/replayer/CMakeLists.txt index 91b97c63..18938b1f 100644 --- a/components/replayer/CMakeLists.txt +++ b/components/replayer/CMakeLists.txt @@ -29,6 +29,7 @@ add_sen_package( STL_FILES ${_replayer_stl_files} DEPS sen::db PRIVATE_DEPS spdlog::spdlog + TEST_TARGET replayer_for_testing IS_COMPONENT ) @@ -40,3 +41,8 @@ install(FILES ${_replayer_stl_files} DESTINATION interfaces/stl/sen/components/r sen_internal_install(replayer) install(FILES ${PROJECT_SOURCE_DIR}/schemas/replayer.json DESTINATION schemas) + +# tests +if(SEN_BUILD_TESTS) + add_subdirectory(test) +endif() diff --git a/components/replayer/src/replay.cpp b/components/replayer/src/replay.cpp index 2ba6f1bd..977fa643 100644 --- a/components/replayer/src/replay.cpp +++ b/components/replayer/src/replay.cpp @@ -191,6 +191,11 @@ void ReplayImpl::seekImpl(TimeStamp time) applyCursor(); setNextPlaybackTime(cursor_.get().time); flushObjectsActivity(); + + if (auto diff = time - cursor_.get().time; diff.get() > 0) + { + advanceCursor(diff); + } } void ReplayImpl::applyCursor() diff --git a/components/replayer/test/CMakeLists.txt b/components/replayer/test/CMakeLists.txt new file mode 100644 index 00000000..7f393731 --- /dev/null +++ b/components/replayer/test/CMakeLists.txt @@ -0,0 +1,37 @@ +# === CMakeLists.txt =================================================================================================== +# Sen Infrastructure +# Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +# See the LICENSE.txt file for more information. +# © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +# ====================================================================================================================== + +set(replayer_tests replayer_test.cpp replay_test.cpp replayed_object_test.cpp) + +add_sen_unit_test_suite( + replayer_test + ${replayer_tests} + LINK_DEPS + sen::core + sen::kernel + sen::db + replayer_for_testing + archive_test_helpers +) + +sen_generate_cpp( + TARGET replayer_test + STL_FILES stl/replay_test_class.stl + GEN_HDR_FILES public_generated_files +) + +target_include_directories( + replayer_test SYSTEM + INTERFACE + PRIVATE ${CMAKE_SOURCE_DIR}/components/replayer/src +) + +target_compile_definitions(replayer_test PRIVATE TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data") + +set_target_properties( + replayer_test PROPERTIES FOLDER "test" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin" +) diff --git a/components/replayer/test/replay_test.cpp b/components/replayer/test/replay_test.cpp new file mode 100644 index 00000000..bd7ee393 --- /dev/null +++ b/components/replayer/test/replay_test.cpp @@ -0,0 +1,456 @@ +// === replay_test.cpp ================================================================================================= +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +// component +#include "replay.h" +#include "replayer_test_helpers.h" + +// sen +#include "sen/core/base/compiler_macros.h" +#include "sen/core/base/duration.h" +#include "sen/core/base/timestamp.h" +#include "sen/core/obj/interest.h" +#include "sen/core/obj/object.h" +#include "sen/core/obj/object_list.h" +#include "sen/core/obj/object_source.h" +#include "sen/core/obj/subscription.h" +#include "sen/db/input.h" +#include "sen/db/output.h" +#include "sen/kernel/component.h" +#include "sen/kernel/component_api.h" +#include "sen/kernel/test_kernel.h" + +// generated code +#include "stl/replay_test_class.stl.h" +#include "stl/sen/components/replayer/replayer.stl.h" +#include "stl/sen/db/basic_types.stl.h" + +// google test +#include + +// std +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace replayer::test +{ +using sen::components::replayer::ReplayStatus; + +class TestReplayImpl final: public sen::components::replayer::ReplayImpl +{ +public: + SEN_NOCOPY_NOMOVE(TestReplayImpl) + using ReplayImpl::ReplayImpl; + ~TestReplayImpl() override = default; + + void playDirect() { playImpl(); } + void pauseDirect() { pauseImpl(); } + void stopDirect() { stopImpl(); } + void advanceDirect(sen::Duration time) { advanceImpl(time); } + void seekDirect(sen::TimeStamp time) { seekImpl(time); } +}; + +struct ReplaySetup +{ + SEN_NOCOPY_NOMOVE(ReplaySetup) + TempDir tempDir; // NOLINT(misc-non-private-member-variables-in-classes) + std::shared_ptr controlBus; // NOLINT(misc-non-private-member-variables-in-classes) + std::shared_ptr replay; // NOLINT(misc-non-private-member-variables-in-classes) + sen::kernel::TestComponent component; // NOLINT(misc-non-private-member-variables-in-classes) + std::unique_ptr kernel; // NOLINT(misc-non-private-member-variables-in-classes) + + std::filesystem::path archivePath; // NOLINT(misc-non-private-member-variables-in-classes) + + explicit ReplaySetup(const std::string& archiveName = "test_replay", + std::function populateDb = nullptr) + { + archivePath = makeArchivePath(archiveName, tempDir); + auto settings = makeArchiveSettings(archiveName, tempDir); + + { + sen::db::Output output(std::move(settings), []() {}); + if (populateDb) + { + sen::kernel::TestComponent dummyComponent; + sen::kernel::TestKernel dummyKernel(&dummyComponent); + populateDb(output, dummyKernel); + } + else + { + sen::TimeStamp t(std::chrono::seconds(0)); + output.keyframe(t, {}); + t += std::chrono::seconds(10); + output.keyframe(t, {}); + t += std::chrono::seconds(10); + output.keyframe(t, {}); + } + } + + component.onInit( + [this](sen::kernel::InitApi&& api) -> sen::kernel::PassResult + { + controlBus = api.getSource("local.replay_test"); + return sen::kernel::done(); + }); + + component.onRun( + [this, archiveName](sen::kernel::RunApi& api) + { + auto input = std::make_unique(archivePath.string(), api.getTypes()); + replay = std::make_shared(archiveName, archivePath.string(), std::move(input), api); + controlBus->add(replay); + return api.execLoop(std::chrono::seconds(1), + [this, &api]() + { + if (replay) + { + replay->update(api); + } + }); + }); + + kernel = std::make_unique(&component); + } + + ~ReplaySetup() + { + if (controlBus != nullptr && replay != nullptr) + { + replay->stopDirect(); + controlBus->remove(replay); + + step(2); + replay.reset(); + } + + controlBus.reset(); + kernel.reset(); + } + + void step(std::size_t count = 1) const { kernel->step(count); } +}; + +void registerDummyReplayType(ReplaySetup& setup) +{ + setup.kernel->getTypes().add(replayer_test::DummyReplayObjBase::meta()); +} + +[[nodiscard]] TestReplayImpl& startReplay(ReplaySetup& setup) +{ + setup.step(); + if (!setup.replay) + { + throw std::runtime_error("startReplay: replay was not created"); + } + return *setup.replay; +} + +[[nodiscard]] sen::Subscription observeReplayObjects(const ReplaySetup& setup) +{ + if (setup.controlBus == nullptr) + { + throw std::runtime_error("observeReplayObjects: control bus was not created"); + } + + sen::Subscription subscription; + subscription.attachTo( + setup.controlBus, sen::Interest::make("SELECT * FROM local.replay_test", sen::CustomTypeRegistry()), false); + return subscription; +} + +[[nodiscard]] bool hasObjectNamed(const sen::ObjectList& objects, std::string_view name) +{ + for (const auto* object: objects.getObjects()) + { + if (object != nullptr && object->getName() == name) + { + return true; + } + } + return false; +} + +[[nodiscard]] std::size_t countObjectsNamed(const sen::ObjectList& objects, std::string_view name) +{ + std::size_t count = 0; + for (const auto* object: objects.getObjects()) + { + if (object != nullptr && object->getName() == name) + { + ++count; + } + } + return count; +} + +/// @test +/// Keeps playback time coherent across play, pause, stop, and reset +/// requirements(SEN-364) +TEST(ReplayTest, StopChangesStatusAndResetsTime) +{ + ReplaySetup setup; + auto& replay = startReplay(setup); + + EXPECT_EQ(replay.getNextStatus(), ReplayStatus::stopped); + + auto initialTime = replay.getNextPlaybackTime(); + + replay.playDirect(); + setup.step(); + EXPECT_EQ(replay.getNextStatus(), ReplayStatus::playing); + EXPECT_NO_THROW(replay.playDirect()); + + replay.pauseDirect(); + setup.step(); + EXPECT_EQ(replay.getNextStatus(), ReplayStatus::paused); + EXPECT_NO_THROW(replay.pauseDirect()); + + replay.advanceDirect(std::chrono::seconds(10)); + + EXPECT_GT(replay.getNextPlaybackTime(), initialTime); + + replay.stopDirect(); + setup.step(); + EXPECT_EQ(replay.getNextStatus(), ReplayStatus::stopped); + EXPECT_EQ(replay.getNextPlaybackTime(), initialTime); + + EXPECT_NO_THROW(replay.stopDirect()); +} + +/// @test +/// Advances when idle, ignores manual advance while playing and pauses at the end +/// requirements(SEN-364) +TEST(ReplayTest, AdvanceForwardWhenPaused) +{ + ReplaySetup setup; + auto& replay = startReplay(setup); + + auto initialTime = replay.getNextPlaybackTime(); + replay.advanceDirect(std::chrono::seconds(5)); + setup.step(); + + EXPECT_EQ(replay.getNextPlaybackTime(), initialTime + std::chrono::seconds(5)); + + replay.playDirect(); + setup.step(); + auto playingTime = replay.getNextPlaybackTime(); + + replay.advanceDirect(std::chrono::seconds(10)); + setup.step(); + + EXPECT_LT(replay.getNextPlaybackTime(), playingTime + std::chrono::seconds(5)); + + ReplaySetup eofSetup; + auto& eofReplay = startReplay(eofSetup); + + eofReplay.advanceDirect(std::chrono::seconds(40)); + EXPECT_EQ(eofReplay.getNextStatus(), ReplayStatus::paused); +} + +/// @test +/// Seeks within the archive window and rejects times outside it +/// requirements(SEN-364) +TEST(ReplayTest, SeekingToValidTimeUpdatesTime) +{ + ReplaySetup setup; + auto& replay = startReplay(setup); + + auto initialTime = replay.getNextPlaybackTime(); + sen::TimeStamp seekTarget = initialTime; + + EXPECT_NO_THROW(replay.seekDirect(seekTarget)); + setup.step(); + + EXPECT_EQ(replay.getNextPlaybackTime(), seekTarget); + + sen::TimeStamp outOfBounds = initialTime + std::chrono::hours(1); + EXPECT_ANY_THROW(replay.seekDirect(outOfBounds)); +} + +/// @test +/// Seeks to a time between two indexed keyframes +/// requirements(SEN-364) +TEST(ReplayTest, SeekToIntermediateTime) +{ + ReplaySetup setup("test_seek", + [](sen::db::Output& output, sen::kernel::TestKernel&) + { + auto t = makeTime(0); + output.keyframe(t, {}); + + auto dummyObj = std::make_shared("dummy", sen::VarMap {}); + auto info = makeObjectInfo(dummyObj, "local", "dummy_registry"); + + t += std::chrono::seconds(10); + output.creation(t, info, true); + output.propertyChange(t, dummyObj->getId(), firstPropertyId(*dummyObj), {}); + output.keyframe(t, {info}); + + t += std::chrono::seconds(10); + output.keyframe(t, {info}); + }); + + registerDummyReplayType(setup); + auto& replay = startReplay(setup); + + auto initialTime = replay.getNextPlaybackTime(); + + sen::TimeStamp seekTarget = initialTime + std::chrono::seconds(15); + + EXPECT_NO_THROW(replay.seekDirect(seekTarget)); + setup.step(); + + EXPECT_EQ(replay.getNextPlaybackTime(), seekTarget); +} + +/// @test +/// Handles keyframe replacement and awkward lifecycle entries without breaking playback +/// requirements(SEN-364) +TEST(ReplayTest, KeyframeHandling) +{ + ReplaySetup setup("test_keyframe", + [](sen::db::Output& output, sen::kernel::TestKernel&) + { + auto obj1 = std::make_shared("obj1", sen::VarMap {}); + auto info1 = makeObjectInfo(obj1, "local", "replay_test"); + auto obj2 = std::make_shared("obj2", sen::VarMap {}); + auto info2 = makeObjectInfo(obj2, "local", "replay_test"); + + auto t = makeTime(0); + output.keyframe(t, {info1}); + + t += std::chrono::seconds(10); + output.keyframe(t, {info2}); + + t += std::chrono::seconds(10); + output.keyframe(t, {info2}); + }); + + registerDummyReplayType(setup); + auto& replay = startReplay(setup); + auto objects = observeReplayObjects(setup); + + EXPECT_FALSE(hasObjectNamed(objects.list, "obj1")); + EXPECT_FALSE(hasObjectNamed(objects.list, "obj2")); + + replay.advanceDirect(std::chrono::seconds(10)); + setup.step(); + + EXPECT_FALSE(hasObjectNamed(objects.list, "obj1")); + EXPECT_TRUE(hasObjectNamed(objects.list, "obj2")); + EXPECT_EQ(countObjectsNamed(objects.list, "obj2"), 1U); + + replay.advanceDirect(std::chrono::seconds(10)); + setup.step(); + + EXPECT_FALSE(hasObjectNamed(objects.list, "obj1")); + EXPECT_TRUE(hasObjectNamed(objects.list, "obj2")); + EXPECT_EQ(countObjectsNamed(objects.list, "obj2"), 1U); + + ReplaySetup lifecycleSetup("test_lifecycle", + [](sen::db::Output& output, sen::kernel::TestKernel&) + { + auto obj1 = std::make_shared("obj1", sen::VarMap {}); + auto info1 = makeObjectInfo(obj1, "local", "replay_test"); + + auto t = makeTime(0); + output.creation(t, info1, true); + + t += std::chrono::seconds(5); + output.creation(t, info1, true); + + t += std::chrono::seconds(5); + output.deletion(t, obj1->getId()); + + t += std::chrono::seconds(5); + output.deletion(t, sen::ObjectId(999)); + }); + + registerDummyReplayType(lifecycleSetup); + auto& lifecycleReplay = startReplay(lifecycleSetup); + auto lifecycleObjects = observeReplayObjects(lifecycleSetup); + lifecycleReplay.advanceDirect(std::chrono::seconds(1)); + + lifecycleSetup.step(1); + EXPECT_TRUE(hasObjectNamed(lifecycleObjects.list, "obj1")); + + lifecycleReplay.advanceDirect(std::chrono::seconds(10)); + lifecycleSetup.step(1); + EXPECT_FALSE(hasObjectNamed(lifecycleObjects.list, "obj1")); +} + +/// @test +/// Applies payloads for existing objects and ignores the ones that arrive too late +/// requirements(SEN-364) +TEST(ReplayTest, PayloadApplication) +{ + ReplaySetup setup("test_payloads", + [](sen::db::Output& output, sen::kernel::TestKernel&) + { + auto obj1 = std::make_shared("obj1", sen::VarMap {}); + auto info1 = makeObjectInfo(obj1, "local", "replay_test"); + auto obj2 = std::make_shared("obj2", sen::VarMap {}); + auto info2 = makeObjectInfo(obj2, "local", "replay_test"); + + auto t = makeTime(0); + output.creation(t, info1, true); + output.creation(t, info2, true); + + t += std::chrono::seconds(5); + output.propertyChange(t, obj1->getId(), firstPropertyId(*obj1), {}); + + t += std::chrono::seconds(5); + output.deletion(t, obj2->getId()); + + t += std::chrono::seconds(5); + output.propertyChange(t, obj2->getId(), firstPropertyId(*obj1), {}); + output.event(t, obj2->getId(), firstEventId(*obj1), {}); + + t += std::chrono::seconds(5); + }); + + registerDummyReplayType(setup); + auto& replay = startReplay(setup); + + replay.advanceDirect(std::chrono::seconds(40)); + setup.step(); + + EXPECT_EQ(replay.getNextStatus(), ReplayStatus::paused); +} + +/// @test +/// Rejects seeks that fall inside the time window but outside the keyframe index +/// requirements(SEN-364) +TEST(ReplayTest, SeekNoIndexThrows) +{ + ReplaySetup setup("test_no_index", + [](sen::db::Output& output, sen::kernel::TestKernel&) + { + auto obj1 = std::make_shared("obj1", sen::VarMap {}); + auto info1 = makeObjectInfo(obj1, "local", "replay_test"); + + auto t = makeTime(5); + output.creation(t, info1, false); + + t = makeTime(20); + output.deletion(t, obj1->getId()); + }); + + registerDummyReplayType(setup); + auto& replay = startReplay(setup); + + EXPECT_ANY_THROW(replay.seekDirect(makeTime(10))); +} + +} // namespace replayer::test diff --git a/components/replayer/test/replayed_object_test.cpp b/components/replayer/test/replayed_object_test.cpp new file mode 100644 index 00000000..62f6188b --- /dev/null +++ b/components/replayer/test/replayed_object_test.cpp @@ -0,0 +1,711 @@ +// === replayed_object_test.cpp ======================================================================================== +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +// component +#include "replayed_object.h" +#include "replayed_object_proxy.h" +#include "replayer_test_helpers.h" + +// sen +#include "sen/core/base/compiler_macros.h" +#include "sen/core/base/memory_block.h" +#include "sen/core/base/numbers.h" +#include "sen/core/base/span.h" +#include "sen/core/base/timestamp.h" +#include "sen/core/io/buffer_writer.h" +#include "sen/core/io/input_stream.h" +#include "sen/core/io/output_stream.h" +#include "sen/core/meta/class_type.h" +#include "sen/core/meta/event.h" +#include "sen/core/meta/native_types.h" +#include "sen/core/meta/property.h" +#include "sen/core/meta/sequence_traits.h" +#include "sen/core/meta/type.h" +#include "sen/core/meta/var.h" +#include "sen/core/obj/callback.h" +#include "sen/core/obj/connection_guard.h" +#include "sen/core/obj/detail/work_queue.h" +#include "sen/core/obj/native_object.h" +#include "sen/db/creation.h" +#include "sen/db/event.h" +#include "sen/db/input.h" +#include "sen/db/keyframe.h" +#include "sen/db/output.h" +#include "sen/db/property_change.h" +#include "sen/db/snapshot.h" +#include "sen/kernel/component.h" +#include "sen/kernel/component_api.h" +#include "sen/kernel/test_kernel.h" + +// generated code +#include "stl/sen/db/basic_types.stl.h" +#include "stl/sen/kernel/basic_types.stl.h" + +// google test +#include + +// std +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace replayer::test +{ + +class TestReplayedObject final: public sen::components::replayer::ReplayedObject +{ +public: + SEN_NOCOPY_NOMOVE(TestReplayedObject) + using ReplayedObject::ReplayedObject; + ~TestReplayedObject() override = default; + + using ReplayedObject::commit; + using ReplayedObject::senImplComputeMaxReliableSerializedPropertySizeImpl; + using ReplayedObject::senImplGetFieldValueGetter; + using ReplayedObject::senImplRemoveTypedConnection; + using ReplayedObject::senImplStreamCall; + using ReplayedObject::senImplVariantCall; + using ReplayedObject::senImplWriteAllPropertiesToStream; + using ReplayedObject::senImplWriteChangedPropertiesToStream; + using ReplayedObject::senImplWriteDynamicPropertiesToStream; + using ReplayedObject::senImplWriteStaticPropertiesToStream; +}; + +class TestReplayedObjectProxy final: public sen::components::replayer::ReplayedObjectProxy +{ +public: + SEN_NOCOPY_NOMOVE(TestReplayedObjectProxy) + TestReplayedObjectProxy(sen::components::replayer::ReplayedObject* owner, std::string_view localPrefix) + : ReplayedObjectProxy(owner, localPrefix) + { + eventQueue_.enable(); + + for (const auto& prop: owner->getAllProps()) + { + guards.push_back(onPropertyChangedUntyped( + prop.get(), + sen::EventCallback(&eventQueue_, + [this, id = prop->getId()](const sen::EventInfo&, const sen::VarList&) + { emittedEvents.push_back(id); }))); + } + } + + ~TestReplayedObjectProxy() override = default; + + using ReplayedObjectProxy::drainInputsImpl; + using ReplayedObjectProxy::senImplGetPropertyImpl; + using ReplayedObjectProxy::senImplRemoveTypedConnection; + using ReplayedObjectProxy::senImplWriteAllPropertiesToStream; + using ReplayedObjectProxy::senImplWriteDynamicPropertiesToStream; + using ReplayedObjectProxy::senImplWriteStaticPropertiesToStream; + + void executeCallbacks() + { + while (eventQueue_.executeAll()) + { + } + } + // intercept events for validation + std::vector emittedEvents; // NOLINT(misc-non-private-member-variables-in-classes) + std::vector guards; // NOLINT(misc-non-private-member-variables-in-classes) + +private: + sen::impl::WorkQueue eventQueue_ {256U, false}; +}; + +[[nodiscard]] const sen::Property* findPropertyByName(const TestReplayedObject& obj, std::string_view propertyName) +{ + const auto* property = obj.getClass().type()->searchPropertyByName(std::string(propertyName)); + if (!property) + { + throw std::runtime_error("findPropertyByName: property not found"); + } + return property; +} + +[[nodiscard]] const sen::Event* findEventByName(const TestReplayedObject& obj, std::string_view eventName) +{ + const auto* event = obj.getClass().type()->searchEventByName(std::string(eventName)); + if (!event) + { + throw std::runtime_error("findEventByName: event not found"); + } + return event; +} + +void executeQueue(sen::impl::WorkQueue& queue) +{ + while (queue.executeAll()) + { + } +} + +void executeObjectQueue(sen::NativeObject* object) +{ + auto* queue = sen::impl::getWorkQueue(object); + if (!queue) + { + throw std::runtime_error("executeObjectQueue: object queue is null"); + } + queue->enable(); + executeQueue(*queue); +} + +[[nodiscard]] std::shared_ptr makeObject(const sen::db::Snapshot& snapshot, + sen::TimeStamp timeStamp) +{ + return std::make_shared(snapshot, timeStamp); +} + +template +void injectFlushCommit(TestReplayedObject& object, sen::TimeStamp timeStamp, const T& entry) +{ + object.inject(timeStamp, entry); + object.flushPendingChanges(timeStamp); + object.commit(timeStamp); +} + +void flushCommit(TestReplayedObject& object, sen::TimeStamp timeStamp) +{ + object.flushPendingChanges(timeStamp); + object.commit(timeStamp); +} + +struct ReplayedObjectSetup +{ + TempDir tempDir; // NOLINT(misc-non-private-member-variables-in-classes) + std::shared_ptr object; // NOLINT(misc-non-private-member-variables-in-classes) + sen::kernel::TestComponent component; // NOLINT(misc-non-private-member-variables-in-classes) + std::unique_ptr kernel; // NOLINT(misc-non-private-member-variables-in-classes) + std::unique_ptr input; // NOLINT(misc-non-private-member-variables-in-classes) + std::filesystem::path archivePath; // NOLINT(misc-non-private-member-variables-in-classes) + + template + void writePropertyChange(sen::db::Output& output, + sen::TimeStamp timeStamp, + std::string_view propertyName, + Writer&& writer) + { + const auto* property = object->getClass().type()->searchPropertyByName(std::string(propertyName)); + if (!property) + { + throw std::runtime_error("writePropertyChange: property not found"); + } + + ::sen::kernel::Buffer buffer; + sen::ResizableBufferWriter bufferWriter(buffer); + sen::OutputStream out(bufferWriter); + writer(out); + output.propertyChange(timeStamp, object->getId(), property->getId(), std::move(buffer)); + } + + template + void writeEvent(sen::db::Output& output, sen::TimeStamp timeStamp, std::string_view eventName, Writer&& writer) + { + const auto* event = object->getClass().type()->searchEventByName(std::string(eventName)); + if (!event) + { + throw std::runtime_error("writeEvent: event not found"); + } + + ::sen::kernel::Buffer buffer; + sen::ResizableBufferWriter bufferWriter(buffer); + sen::OutputStream out(bufferWriter); + writer(out); + output.event(timeStamp, object->getId(), event->getId(), std::move(buffer)); + } + + ReplayedObjectSetup() + { + // register an object + object = std::make_shared("dummyObj", sen::VarMap {}); + component.onInit( + [this](sen::kernel::InitApi&& api) -> sen::kernel::PassResult + { + auto source = api.getSource("local.test"); + source->add(object); + return sen::kernel::done(); + }); + component.onRun([](auto& api) { return api.execLoop(std::chrono::seconds(1), []() {}); }); + kernel = std::make_unique(&component); + kernel->step(); + + // write archive to disk + archivePath = makeArchivePath("replayed_obj_test", tempDir); + auto settings = makeArchiveSettings("replayed_obj_test", tempDir); + + { + sen::db::Output output(std::move(settings), []() {}); + + auto info = makeObjectInfo(object); + output.creation(kernel->getTime(), info, true); + + kernel->step(); + const auto entryTime = kernel->getTime(); + + writePropertyChange(output, + entryTime, + "testProp", + [](sen::OutputStream& out) { sen::SerializationTraits::write(out, 12.3); }); + writePropertyChange( + output, entryTime, "uniProp", [](sen::OutputStream& out) { sen::SerializationTraits::write(out, 1); }); + writePropertyChange(output, + entryTime, + "multiProp", + [](sen::OutputStream& out) { sen::SerializationTraits::write(out, 2); }); + writePropertyChange(output, + entryTime, + "enumProp", + [](sen::OutputStream& out) { sen::SerializationTraits::write(out, 1U); }); + writePropertyChange(output, + entryTime, + "distProp", + [](sen::OutputStream& out) { sen::SerializationTraits::write(out, 2.5F); }); + writePropertyChange(output, + entryTime, + "seqProp", + [](sen::OutputStream& out) + { + std::vector values {1, 2, 3}; + sen::SequenceTraitsBase>::write(out, values); + }); + writePropertyChange(output, entryTime, "emptyStructProp", [](sen::OutputStream& out) { out.writeUInt8(0U); }); + + writeEvent(output, + entryTime, + "testEvent", + [](sen::OutputStream& out) + { + sen::SerializationTraits::write(out, 99); + sen::SerializationTraits::write(out, "hello"); + }); + + kernel->step(); + output.keyframe(kernel->getTime(), {info}); + } + + // open the archive for reading + input = std::make_unique(archivePath.string(), kernel->getTypes()); + } + + [[nodiscard]] sen::db::Snapshot findCreationSnapshot() const + { + auto cursor = input->begin(); + while (!cursor.atEnd()) + { + ++cursor; + if (cursor.atEnd()) + { + break; + } + + const auto& entry = cursor.get(); + if (std::holds_alternative(entry.payload)) + { + return std::get(entry.payload).getSnapshot(); + } + } + throw std::runtime_error("no Creation entry found in the archive"); + } + + [[nodiscard]] sen::db::PropertyChange findPropertyChange() const { return findPropertyChangeNamed("testProp"); } + + [[nodiscard]] sen::db::PropertyChange findPropertyChangeNamed(const std::string& name) const + { + auto cursor = input->begin(); + while (!cursor.atEnd()) + { + if (std::holds_alternative(cursor.get().payload)) + { + const auto& pc = std::get(cursor.get().payload); + if (pc.getProperty()->getName() == name) + { + return pc; + } + } + ++cursor; + } + throw std::runtime_error("no PropertyChange entry found in the archive"); + } + + [[nodiscard]] sen::db::Event findEvent() const + { + auto cursor = input->begin(); + while (!cursor.atEnd()) + { + if (std::holds_alternative(cursor.get().payload)) + { + return std::get(cursor.get().payload); + } + ++cursor; + } + throw std::runtime_error("no Event entry found in the archive"); + } + + [[nodiscard]] sen::db::Snapshot findKeyframeSnapshot() const + { + auto cursor = input->begin(); + while (!cursor.atEnd()) + { + ++cursor; + if (cursor.atEnd()) + { + break; + } + + if (const auto& entry = cursor.get(); std::holds_alternative(entry.payload)) + { + if (const auto& kf = std::get(entry.payload); !kf.getSnapshots().empty()) + { + return kf.getSnapshots()[0]; + } + } + } + throw std::runtime_error("no Keyframe with snapshots found in the archive"); + } +}; + +/// @test +/// Builds a replayed object from a snapshot and preserves its initial state +/// requirements(SEN-364) +TEST(ReplayedObjectTest, ConstructionFromSnapshotSetsName) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findCreationSnapshot(); + const auto t0 = makeTime(3); + auto obj = makeObject(snapshot, t0); + + EXPECT_EQ(obj->getName(), "dummyObj"); + ASSERT_NE(obj->getClass().type(), nullptr); + EXPECT_EQ(obj->getClass().type()->getQualifiedName(), snapshot.getType().type()->getQualifiedName()); + EXPECT_EQ(obj->getLastCommitTime(), t0); + EXPECT_FALSE(obj->getAllProps().empty()); + + const auto* prop = findPropertyByName(*obj, "testProp"); + EXPECT_FALSE(obj->getPropertyUntyped(prop).isEmpty()); + EXPECT_EQ(obj->getPropertyLastTime(prop), t0); +} + +/// @test +/// Applies injected changes only after flush and commit +/// requirements(SEN-364) +TEST(ReplayedObjectTest, FlushAndCommitAppliesPropertyChange) +{ + ReplayedObjectSetup setup; + auto creationSnapshot = setup.findCreationSnapshot(); + auto keyframeSnapshot = setup.findKeyframeSnapshot(); + const auto t0 = makeTime(1); + auto obj = makeObject(creationSnapshot, t0); + const auto* prop = findPropertyByName(*obj, "testProp"); + auto originalValue = obj->getPropertyUntyped(prop).getCopyAs(); + + auto propChange = setup.findPropertyChange(); + const auto t1 = makeTime(5); + obj->inject(t1, propChange); + EXPECT_EQ(obj->getPropertyUntyped(prop).getCopyAs(), originalValue); + + flushCommit(*obj, t1); + EXPECT_DOUBLE_EQ(obj->getPropertyUntyped(prop).getCopyAs(), 12.3); + EXPECT_EQ(obj->getPropertyLastTime(prop), t1); + + const auto t2 = makeTime(9); + injectFlushCommit(*obj, t2, keyframeSnapshot); + EXPECT_EQ(obj->getLastCommitTime(), t2); + + const auto t3 = makeTime(12); + injectFlushCommit(*obj, t3, propChange); + EXPECT_EQ(obj->getLastCommitTime(), t3); + EXPECT_EQ(obj->getPropertyLastTime(prop), t3); + EXPECT_DOUBLE_EQ(obj->getPropertyUntyped(prop).getCopyAs(), 12.3); +} + +/// @test +/// Rejects mutable operations on replayed objects +/// requirements(SEN-364) +TEST(ReplayedObjectTest, RemoveTypedConnectionThrowsLogicError) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findCreationSnapshot(); + auto obj = makeObject(snapshot, makeTime(1)); + const auto* prop = findPropertyByName(*obj, "testProp"); + + EXPECT_THROW(obj->senImplRemoveTypedConnection(sen::ConnId {1}), std::logic_error); + EXPECT_ANY_THROW(std::ignore = obj->getNextPropertyUntyped(prop)); + EXPECT_ANY_THROW(obj->setNextPropertyUntyped(prop, sen::Var(1.0))); +} + +/// @test +/// Serializes replayed state and processes injected changes +/// requirements(SEN-364) +TEST(ReplayedObjectTest, SerializesStateAndProcessesInjectedChanges) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findCreationSnapshot(); + const auto t0 = makeTime(1); + auto obj = makeObject(snapshot, t0); + + { + ::sen::kernel::Buffer allBuffer; + sen::ResizableBufferWriter writer(allBuffer); + sen::OutputStream out(writer); + obj->senImplWriteAllPropertiesToStream(out); + EXPECT_FALSE(allBuffer.empty()); + } + { + ::sen::kernel::Buffer staticBuffer; + sen::ResizableBufferWriter writer(staticBuffer); + sen::OutputStream out(writer); + obj->senImplWriteStaticPropertiesToStream(out); + EXPECT_TRUE(staticBuffer.empty()); + } + { + ::sen::kernel::Buffer dynamicBuffer; + sen::ResizableBufferWriter writer(dynamicBuffer); + sen::OutputStream out(writer); + obj->senImplWriteDynamicPropertiesToStream(out); + EXPECT_FALSE(dynamicBuffer.empty()); + } + + auto propChange = setup.findPropertyChangeNamed("testProp"); + const auto t1 = makeTime(4); + injectFlushCommit(*obj, t1, propChange); + auto* prop = findPropertyByName(*obj, "testProp"); + EXPECT_DOUBLE_EQ(obj->getPropertyUntyped(prop).getCopyAs(), 12.3); + + // Fresh object to isolate serialization state. + auto serializationObj = makeObject(snapshot, t0); + + auto uniChange = setup.findPropertyChangeNamed("uniProp"); + auto multiChange = setup.findPropertyChangeNamed("multiProp"); + serializationObj->inject(t1, propChange); + serializationObj->inject(t1, uniChange); + serializationObj->inject(t1, multiChange); + serializationObj->flushPendingChanges(t1); + + auto maxSize = serializationObj->senImplComputeMaxReliableSerializedPropertySizeImpl(); + EXPECT_GE(maxSize, 0U); + + auto pool = sen::FixedMemoryBlockPool<1024>::make(); + auto uniBlock = pool->getBlockPtr(); + auto multiBlock = pool->getBlockPtr(); + uint32_t uniCalls = 0; + uint32_t multiCalls = 0; + auto uniProvider = [&](uint32_t size) + { + ++uniCalls; + uniBlock->resize(size); + return sen::ResizableBufferWriter(*uniBlock); + }; + auto multiProvider = [&](uint32_t size) + { + ++multiCalls; + multiBlock->resize(size); + return sen::ResizableBufferWriter(*multiBlock); + }; + + std::vector confirmedBuf; + sen::ResizableBufferWriter confirmedWriter(confirmedBuf); + sen::OutputStream confirmedOut(confirmedWriter); + serializationObj->senImplWriteChangedPropertiesToStream(confirmedOut, uniProvider, multiProvider); + + const auto serializedDestinations = uniCalls + multiCalls + static_cast(!confirmedBuf.empty()); + EXPECT_GE(serializedDestinations, 1U); + + auto* objQueue = sen::impl::getWorkQueue(obj.get()); + ASSERT_NE(objQueue, nullptr); + objQueue->enable(); + + auto evt = setup.findEvent(); + const auto t2 = makeTime(7); + obj->inject(t2, evt); + obj->flushPendingChanges(t2); + executeQueue(*objQueue); + objQueue->clear(); + objQueue->disable(); +} + +/// @test +/// Allows getter calls and rejects unknown method ids +/// requirements(SEN-364) +TEST(ReplayedObjectTest, StreamCallToGetterSucceeds) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findCreationSnapshot(); + auto obj = makeObject(snapshot, makeTime(1)); + const auto* prop = findPropertyByName(*obj, "testProp"); + const auto methodId = prop->getGetterMethod().getId(); + + std::vector emptyBuf; + sen::InputStream in(emptyBuf); + + bool streamCallbackInvoked = false; + obj->senImplStreamCall(methodId, + in, + [&streamCallbackInvoked](sen::StreamCall&& streamWriter) + { + ::sen::kernel::Buffer resultBuf; + sen::ResizableBufferWriter writer(resultBuf); + sen::OutputStream out(writer); + streamWriter(out); + streamCallbackInvoked = true; + }); + EXPECT_TRUE(streamCallbackInvoked); + + bool variantCallbackInvoked = false; + sen::Var result; + obj->senImplVariantCall(methodId, + sen::VarList {}, + [&variantCallbackInvoked, &result](sen::VariantCall&& variantWriter) + { + variantWriter(result); + variantCallbackInvoked = true; + }); + EXPECT_TRUE(variantCallbackInvoked); + EXPECT_FALSE(result.isEmpty()); + + sen::MemberHash wrongStreamId(1234); + sen::MemberHash wrongVariantId(4321); + EXPECT_ANY_THROW(obj->senImplStreamCall(wrongStreamId, in, [](sen::StreamCall&&) {})); + EXPECT_ANY_THROW(obj->senImplVariantCall(wrongVariantId, sen::VarList {}, [](sen::VariantCall&&) {})); +} + +/// @test +/// Resolves supported field getters and rejects invalid paths +/// requirements(SEN-364) +TEST(ReplayedObjectTest, FieldGetterHandlesSupportedAndInvalidPaths) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findKeyframeSnapshot(); + auto obj = makeObject(snapshot, makeTime(1)); + + auto expectFieldGetterWorks = + [&](const std::string& name, std::vector path = {}, bool runtimeErrorIsExpected = false) + { + const auto* property = obj->getClass().type()->searchPropertyByName(name); + ASSERT_NE(property, nullptr); + try + { + auto result = obj->senImplGetFieldValueGetter(property->getId(), sen::Span(path.data(), path.size())); + EXPECT_TRUE(result.getterFunc != nullptr); + } + catch (const std::runtime_error& err) + { + if (runtimeErrorIsExpected) + { + SUCCEED(); + } + else + { + ADD_FAILURE() << "Unexpected runtime_error in expectFieldGetterWorks(" << name << "): " << err.what(); + } + } + }; + + expectFieldGetterWorks("bProp"); + expectFieldGetterWorks("u8Prop"); + expectFieldGetterWorks("i16Prop"); + expectFieldGetterWorks("u16Prop"); + expectFieldGetterWorks("i32Prop"); + expectFieldGetterWorks("u32Prop"); + expectFieldGetterWorks("i64Prop"); + expectFieldGetterWorks("u64Prop"); + expectFieldGetterWorks("f32Prop"); + expectFieldGetterWorks("testProp"); + expectFieldGetterWorks("strProp"); + expectFieldGetterWorks("durProp"); + expectFieldGetterWorks("timeProp"); + expectFieldGetterWorks("enumProp", {}, true); + expectFieldGetterWorks("distProp", {}, true); + expectFieldGetterWorks("structProp", {0}); + expectFieldGetterWorks("structProp", {1, 0}); + + const auto* variantProp = findPropertyByName(*obj, "variantProp"); + auto canReadVariantField = [&](uint16_t index) + { + std::vector path {index}; + try + { + auto result = + obj->senImplGetFieldValueGetter(variantProp->getId(), sen::Span(path.data(), path.size())); + return result.getterFunc != nullptr; + } + catch (...) + { + return false; + } + }; + EXPECT_TRUE(canReadVariantField(0) || canReadVariantField(1)); + + EXPECT_ANY_THROW(static_cast( + obj->senImplGetFieldValueGetter(findPropertyByName(*obj, "seqProp")->getId(), sen::Span()))); + + std::vector badStructPath {99U}; + EXPECT_ANY_THROW(static_cast(obj->senImplGetFieldValueGetter( + findPropertyByName(*obj, "structProp")->getId(), sen::Span(badStructPath.data(), badStructPath.size())))); + + std::vector badVariantPath {99U}; + EXPECT_ANY_THROW(static_cast( + obj->senImplGetFieldValueGetter(findPropertyByName(*obj, "variantProp")->getId(), + sen::Span(badVariantPath.data(), badVariantPath.size())))); + + std::vector emptyStructPath {0U}; + EXPECT_ANY_THROW(static_cast( + obj->senImplGetFieldValueGetter(findPropertyByName(*obj, "emptyStructProp")->getId(), + sen::Span(emptyStructPath.data(), emptyStructPath.size())))); +} + +/// @test +/// Proxy mirrors committed changes and blocks invalid native operations +/// requirements(SEN-364) +TEST(ReplayedObjectTest, ProxyDrainInputsDetectsChanges) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findCreationSnapshot(); + const auto t0 = makeTime(1); + auto obj = makeObject(snapshot, t0); + TestReplayedObjectProxy proxy(obj.get(), "local"); + + const auto* prop = findPropertyByName(*obj, "testProp"); + auto val = proxy.senImplGetPropertyImpl(prop->getId()); + EXPECT_FALSE(val.isEmpty()); + EXPECT_EQ(val.getCopyAs(), obj->getPropertyUntyped(prop).getCopyAs()); + + proxy.drainInputsImpl(t0); + proxy.executeCallbacks(); + EXPECT_TRUE(proxy.emittedEvents.empty()); + + auto propChange = setup.findPropertyChangeNamed("testProp"); + const auto t1 = makeTime(5); + injectFlushCommit(*obj, t1, propChange); + + proxy.drainInputsImpl(t1); + proxy.executeCallbacks(); + ASSERT_EQ(proxy.emittedEvents.size(), 1); + EXPECT_EQ(proxy.emittedEvents.front(), prop->getId()); + + proxy.emittedEvents.clear(); + proxy.drainInputsImpl(t1); + proxy.executeCallbacks(); + EXPECT_TRUE(proxy.emittedEvents.empty()); + + std::vector buf; + sen::ResizableBufferWriter writer(buf); + sen::OutputStream out(writer); + EXPECT_THROW(proxy.senImplWriteAllPropertiesToStream(out), std::logic_error); + EXPECT_THROW(proxy.senImplWriteStaticPropertiesToStream(out), std::logic_error); + EXPECT_THROW(proxy.senImplWriteDynamicPropertiesToStream(out), std::logic_error); + EXPECT_THROW(proxy.senImplRemoveTypedConnection(sen::ConnId {1}), std::logic_error); +} + +} // namespace replayer::test diff --git a/components/replayer/test/replayer_test.cpp b/components/replayer/test/replayer_test.cpp new file mode 100644 index 00000000..5c4dc5d2 --- /dev/null +++ b/components/replayer/test/replayer_test.cpp @@ -0,0 +1,218 @@ +// === replayer_test.cpp =============================================================================================== +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +// components +#include "replayer.h" +#include "replayer_test_helpers.h" + +// sen +#include "sen/core/obj/object_source.h" +#include "sen/db/output.h" +#include "sen/kernel/component.h" +#include "sen/kernel/component_api.h" +#include "sen/kernel/test_kernel.h" + +// generated code +#include "stl/sen/db/basic_types.stl.h" + +// google test +#include + +// std +#include +#include +#include +#include +#include +#include + +namespace replayer::test +{ + +class TestReplayerImpl final: public sen::components::replayer::ReplayerImpl +{ +public: + using ReplayerImpl::ReplayerImpl; + + TestReplayerImpl(const TestReplayerImpl&) = delete; + TestReplayerImpl& operator=(const TestReplayerImpl&) = delete; + TestReplayerImpl(TestReplayerImpl&&) = delete; + TestReplayerImpl& operator=(TestReplayerImpl&&) = delete; + ~TestReplayerImpl() override = default; + + void openDirect(const std::string& name, const std::string& path) { openImpl(name, path); } + void closeDirect(const std::string& name) { closeImpl(name); } + void closeAllDirect() { closeAllImpl(); } +}; + +struct ReplayerSetup +{ + TempDir tempDir; // NOLINT(misc-non-private-member-variables-in-classes) + std::shared_ptr controlBus; // NOLINT(misc-non-private-member-variables-in-classes) + std::shared_ptr replayer; // NOLINT(misc-non-private-member-variables-in-classes) + sen::kernel::TestComponent component; // NOLINT(misc-non-private-member-variables-in-classes) + std::unique_ptr kernel; // NOLINT(misc-non-private-member-variables-in-classes) + + explicit ReplayerSetup(bool autoPlay = false) + { + component.onInit( + [this](sen::kernel::InitApi&& api) -> sen::kernel::PassResult + { + controlBus = api.getSource("local.replayer"); + return sen::kernel::done(); + }); + + component.onRun( + [this, autoPlay](sen::kernel::RunApi& api) + { + replayer = std::make_shared("test_replayer", autoPlay, controlBus, api); + controlBus->add(replayer); + return api.execLoop(std::chrono::seconds(1), []() {}); + }); + + kernel = std::make_unique(&component); + } + + ReplayerSetup(const ReplayerSetup&) = delete; + ReplayerSetup& operator=(const ReplayerSetup&) = delete; + ReplayerSetup(ReplayerSetup&&) = delete; + ReplayerSetup& operator=(ReplayerSetup&&) = delete; + + ~ReplayerSetup() + { + if (controlBus != nullptr && replayer != nullptr) + { + replayer->closeAllDirect(); + controlBus->remove(replayer); + step(2); + replayer.reset(); + } + controlBus.reset(); + kernel.reset(); + } + + void step(std::size_t count = 1) const { kernel->step(count); } + + [[nodiscard]] std::string createValidArchive(const std::string& name) const + { + const auto fullPath = makeArchivePath(name, tempDir); + auto settings = makeArchiveSettings(name, tempDir); + + sen::db::Output output(std::move(settings), []() {}); + output.keyframe(kernel->getTime(), {}); + + return fullPath.string(); + } +}; + +/// @test +/// Opening a non-exist recording throws error +/// requirements(SEN-364) +TEST(ReplayerTest, OpeningNonExistentRecordingThrows) +{ + ReplayerSetup setup; + setup.step(); + + ASSERT_NE(setup.replayer, nullptr); + + // try to open a non-existing path + const std::string fakePath = (setup.tempDir.path() / "missing_dir").string(); + + EXPECT_ANY_THROW(setup.replayer->openDirect("bad_session", fakePath)); +} + +/// @test +/// Opening a valid recording succeeds +/// requirements(SEN-364) +TEST(ReplayerTest, OpeningValidRecordingSucceeds) +{ + ReplayerSetup setup; + setup.step(); + + const std::string archivePath = setup.createValidArchive("valid_rec"); + + EXPECT_NO_THROW(setup.replayer->openDirect("session_1", archivePath)); + EXPECT_NO_THROW(setup.replayer->closeDirect("session_1")); +} + +/// @test +/// Opening with a duplicate name throws an error +/// requirements(SEN-364) +TEST(ReplayerTest, OpeningWithDuplicateNameThrows) +{ + ReplayerSetup setup; + setup.step(); + + const std::string archivePath1 = setup.createValidArchive("valid_rec1"); + const std::string archivePath2 = setup.createValidArchive("valid_rec2"); + + setup.replayer->openDirect("session_same", archivePath1); + EXPECT_ANY_THROW(setup.replayer->openDirect("session_same", archivePath2)); +} + +/// @test +/// Opening the same archive path twice throws an error +/// requirements(SEN-364) +TEST(ReplayerTest, OpeningWithDuplicatePathThrows) +{ + ReplayerSetup setup; + setup.step(); + + const std::string archivePath = setup.createValidArchive("valid_rec"); + + setup.replayer->openDirect("session_1", archivePath); + EXPECT_ANY_THROW(setup.replayer->openDirect("session_2", archivePath)); +} + +/// @test +/// Closing a non-existent replay throws an error +/// requirements(SEN-364) +TEST(ReplayerTest, ClosingNonExistentReplayThrows) +{ + ReplayerSetup setup; + setup.step(); + + EXPECT_ANY_THROW(setup.replayer->closeDirect("does_not_exist")); +} + +/// @test +/// CloseAll completely clears all managed replays +/// requirements(SEN-364) +TEST(ReplayerTest, CloseAllClearsReplays) +{ + ReplayerSetup setup; + setup.step(); + + const std::string archivePath1 = setup.createValidArchive("valid_rec1"); + const std::string archivePath2 = setup.createValidArchive("valid_rec2"); + + setup.replayer->openDirect("session_1", archivePath1); + setup.replayer->openDirect("session_2", archivePath2); + + setup.replayer->closeAllDirect(); + + // Now they can be reopened because they were cleared, and trying to close throws + EXPECT_ANY_THROW(setup.replayer->closeDirect("session_1")); + EXPECT_ANY_THROW(setup.replayer->closeDirect("session_2")); + + // Adding them again should not throw duplicate path/name errors + EXPECT_NO_THROW(setup.replayer->openDirect("session_1", archivePath1)); +} + +/// @test +/// AutoPlay configuration does not crash when opening an archive +/// requirements(SEN-364) +TEST(ReplayerTest, AutoPlayDoesNotCrash) +{ + ReplayerSetup setup(true); + setup.step(); + + const std::string archivePath = setup.createValidArchive("auto_rec"); + EXPECT_NO_THROW(setup.replayer->openDirect("auto_session", archivePath)); +} + +} // namespace replayer::test diff --git a/components/replayer/test/replayer_test_helpers.h b/components/replayer/test/replayer_test_helpers.h new file mode 100644 index 00000000..8853a77e --- /dev/null +++ b/components/replayer/test/replayer_test_helpers.h @@ -0,0 +1,41 @@ +// === replayer_test_helpers.h ========================================================================================= +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +#ifndef SEN_COMPONENTS_REPLAYER_TEST_REPLAYER_TEST_HELPERS_H +#define SEN_COMPONENTS_REPLAYER_TEST_REPLAYER_TEST_HELPERS_H + +// shared test helpers +#include "archive_test_helpers.h" + +// generated code +#include "stl/replay_test_class.stl.h" + +// sen +#include "sen/core/base/compiler_macros.h" + +namespace replayer::test +{ + +using sen::test::firstEventId; +using sen::test::firstPropertyId; +using sen::test::makeArchivePath; +using sen::test::makeArchiveSettings; +using sen::test::makeObjectInfo; +using sen::test::makeTime; +using sen::test::TempDir; + +class DummyReplayObjImpl: public replayer_test::DummyReplayObjBase +{ +public: + SEN_NOCOPY_NOMOVE(DummyReplayObjImpl) + using DummyReplayObjBase::DummyReplayObjBase; + ~DummyReplayObjImpl() override = default; +}; + +} // namespace replayer::test + +#endif // SEN_COMPONENTS_REPLAYER_TEST_REPLAYER_TEST_HELPERS_H diff --git a/components/replayer/test/stl/replay_test_class.stl b/components/replayer/test/stl/replay_test_class.stl new file mode 100644 index 00000000..92c08630 --- /dev/null +++ b/components/replayer/test/stl/replay_test_class.stl @@ -0,0 +1,59 @@ +package replayer_test; + +struct NestedStruct +{ + val : i32 +} + +struct TestStruct +{ + f1 : i32, + f2 : NestedStruct +} + +variant TestVariant +{ + i32, + string +} + +enum TestEnum: u8 +{ + e0, + e1 +} + +quantity TestDistance; +sequence IntSequence; + +struct EmptyStruct {} + +// Dummy class to inject properties +class DummyReplayObj +{ + var testProp : f64 [confirmed]; + var bProp : bool; + var u8Prop : u8; + var i16Prop : i16; + var u16Prop : u16; + var i32Prop : i32; + var u32Prop : u32; + var i64Prop : i64; + var u64Prop : u64; + var f32Prop : f32; + var strProp : string [confirmed]; + var durProp : Duration; + var timeProp : TimeStamp; + var enumProp : TestEnum [confirmed]; + var distProp : TestDistance [confirmed]; + var seqProp : IntSequence [confirmed]; + var emptyStructProp : EmptyStruct [confirmed]; + + var structProp : TestStruct; + var variantProp : TestVariant [confirmed]; + + var uniProp : i32 [bestEffort]; + var multiProp : i32; + + event testEvent(v1: i32, v2: string) [confirmed]; +} diff --git a/libs/db/src/input.cpp b/libs/db/src/input.cpp index 897cf942..242c6c31 100644 --- a/libs/db/src/input.cpp +++ b/libs/db/src/input.cpp @@ -315,6 +315,11 @@ class Input::Impl { readIndexes(); + if (keyframeIndexes_.empty()) + { + return std::nullopt; + } + auto iterGeq = std::lower_bound(keyframeIndexes_.begin(), keyframeIndexes_.end(), time, @@ -325,6 +330,11 @@ class Input::Impl return *iterGeq; } + if (iterGeq == keyframeIndexes_.end()) + { + return keyframeIndexes_.back(); + } + const auto& prev = *(iterGeq - 1); const auto a = prev.time.sinceEpoch().get(); const auto b = iterGeq->time.sinceEpoch().get(); diff --git a/libs/db/test/CMakeLists.txt b/libs/db/test/CMakeLists.txt index 37feaf7d..45143478 100644 --- a/libs/db/test/CMakeLists.txt +++ b/libs/db/test/CMakeLists.txt @@ -25,6 +25,7 @@ add_sen_unit_test_suite( sen::core sen::kernel sen::db + archive_test_helpers ) sen_generate_cpp( diff --git a/libs/db/test/creation_test.cpp b/libs/db/test/creation_test.cpp index c2bd4934..2958fde8 100644 --- a/libs/db/test/creation_test.cpp +++ b/libs/db/test/creation_test.cpp @@ -35,17 +35,13 @@ TEST(CreationTest, WriteAndReadCreationWithRealObject) TempDir tempDir; SingleClassSetup setup; - OutSettings settings; - settings.name = "test"; - settings.folder = tempDir.path().string(); - settings.indexKeyframes = true; - - const auto archivePath = tempDir.path() / settings.name; + auto settings = makeArchiveSettings("test", tempDir); + const auto archivePath = makeArchivePath("test", tempDir); { Output output(std::move(settings), []() {}); - ObjectInfo info = {setup.object.get(), "test_session", "test_bus"}; + auto info = makeObjectInfo(setup.object); output.creation(setup.kernel->getTime(), info, true); setup.kernel->step(); @@ -92,17 +88,13 @@ TEST(CreationTest, IndexedCreationAppearsInObjectIndex) TempDir tempDir; SingleClassSetup setup; - OutSettings settings; - settings.name = "test"; - settings.folder = tempDir.path().string(); - settings.indexKeyframes = true; - - const auto archivePath = tempDir.path() / settings.name; + auto settings = makeArchiveSettings("test", tempDir); + const auto archivePath = makeArchivePath("test", tempDir); { Output output(std::move(settings), []() {}); - ObjectInfo info = {setup.object.get(), "test_session", "test_bus"}; + auto info = makeObjectInfo(setup.object); output.creation(setup.kernel->getTime(), info, true); setup.kernel->step(); @@ -139,17 +131,13 @@ TEST(CreationTest, NonIndexedCreationNotInObjectIndex) sen::kernel::TestKernel kernel(&component); kernel.step(); - OutSettings settings; - settings.name = "test"; - settings.folder = tempDir.path().string(); - settings.indexKeyframes = true; - - const auto archivePath = tempDir.path() / settings.name; + auto settings = makeArchiveSettings("test", tempDir); + const auto archivePath = makeArchivePath("test", tempDir); { Output output(std::move(settings), []() {}); - ObjectInfo info = {object.get(), "test_session", "test_bus"}; + auto info = makeObjectInfo(object); output.creation(kernel.getTime(), info, false); kernel.step(); output.keyframe(kernel.getTime(), {}); diff --git a/libs/db/test/db_test_helpers.h b/libs/db/test/db_test_helpers.h index e89c597c..891227fa 100644 --- a/libs/db/test/db_test_helpers.h +++ b/libs/db/test/db_test_helpers.h @@ -10,56 +10,33 @@ #ifndef SEN_DB_TEST_HELPERS_H #define SEN_DB_TEST_HELPERS_H -#include "stl/db_test_class.stl.h" +// shared test helpers +#include "archive_test_helpers.h" // sen #include "sen/core/base/compiler_macros.h" -#include "sen/core/base/uuid.h" #include "sen/kernel/component.h" #include "sen/kernel/component_api.h" #include "sen/kernel/test_kernel.h" +// generated code +#include "stl/db_test_class.stl.h" + // std #include -#include #include #include namespace sen::db::test { -class TempDir -{ -public: - TempDir(): path_(std::filesystem::temp_directory_path() / ("db_test_" + getRandomPathPostFix())) - { - std::filesystem::create_directories(path_); - } - - // prevent object copy and movable type - TempDir(const TempDir&) = delete; - TempDir& operator=(const TempDir&) = delete; - - TempDir(TempDir&&) = delete; - TempDir& operator=(TempDir&&) = delete; - - ~TempDir() - { - if (std::filesystem::exists(path_)) - { - std::filesystem::remove_all(path_); - } - } - - [[nodiscard]] const std::filesystem::path& path() const { return path_; } - -private: - /// Returns a random post fix for temporary files paths. - static std::string getRandomPathPostFix() { return sen::UuidRandomGenerator()().toString(); } - -private: - std::filesystem::path path_; -}; +using sen::test::firstEventId; +using sen::test::firstPropertyId; +using sen::test::makeArchivePath; +using sen::test::makeArchiveSettings; +using sen::test::makeObjectInfo; +using sen::test::makeTime; +using sen::test::TempDir; class TestObjImpl: public db_test::TestObjBase { diff --git a/libs/db/test/input_test.cpp b/libs/db/test/input_test.cpp index e2da2695..5caa6178 100644 --- a/libs/db/test/input_test.cpp +++ b/libs/db/test/input_test.cpp @@ -38,12 +38,8 @@ namespace sen::db::test /// Helper function to create a valir archive with one keyframe for corruption tests static std::filesystem::path createValidArchive(const std::filesystem::path& baseDir, sen::kernel::TestKernel& kernel) { - OutSettings settings; - settings.name = "test"; - settings.folder = baseDir.string(); - settings.indexKeyframes = true; - - const auto archivePath = baseDir / settings.name; + auto settings = makeArchiveSettings("test", baseDir); + const auto archivePath = makeArchivePath("test", baseDir); { Output output(std::move(settings), []() {}); @@ -103,17 +99,13 @@ TEST(InputTest, OpenRecordingWithOneKeyframe) const auto* propMeta = classType.type()->searchPropertyByName("speed"); ASSERT_NE(propMeta, nullptr); - OutSettings settings; - settings.name = "test"; - settings.folder = tempDir.path().string(); - settings.indexKeyframes = true; - - const auto archivePath = tempDir.path() / settings.name; + auto settings = makeArchiveSettings("test", tempDir); + const auto archivePath = makeArchivePath("test", tempDir); { Output output(std::move(settings), []() {}); - ObjectInfo info = {object.get(), "test_session", "test_bus"}; + auto info = makeObjectInfo(object); output.creation(kernel.getTime(), info, true); ::sen::kernel::Buffer propBuf; diff --git a/test/support/CMakeLists.txt b/test/support/CMakeLists.txt new file mode 100644 index 00000000..14b9ee7a --- /dev/null +++ b/test/support/CMakeLists.txt @@ -0,0 +1,28 @@ +# === CMakeLists.txt =================================================================================================== +# Sen Infrastructure +# Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +# See the LICENSE.txt file for more information. +# © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +# ====================================================================================================================== + +set(archive_sources src/archive_test_helpers.h src/archive_test_helpers.cpp) + +# ------------------------------------------------------------------------------------------------------------- +# library +# ------------------------------------------------------------------------------------------------------------- + +add_library(archive_test_helpers STATIC ${archive_sources}) + +sen_enable_static_analysis(archive_test_helpers) + +target_include_directories(archive_test_helpers PUBLIC "$") + +target_link_libraries(archive_test_helpers PUBLIC sen::core sen::db) + +# ------------------------------------------------------------------------------------------------------------- +# IDE grouping +# ------------------------------------------------------------------------------------------------------------- + +source_group("sources" FILES ${archive_sources}) + +set_target_properties(archive_test_helpers PROPERTIES FOLDER "test/support") diff --git a/test/support/src/archive_test_helpers.cpp b/test/support/src/archive_test_helpers.cpp new file mode 100644 index 00000000..b9457bae --- /dev/null +++ b/test/support/src/archive_test_helpers.cpp @@ -0,0 +1,9 @@ +// === archive_test_helpers.cpp ======================================================================================== +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +// components +#include "archive_test_helpers.h" diff --git a/test/support/src/archive_test_helpers.h b/test/support/src/archive_test_helpers.h new file mode 100644 index 00000000..b3f52b37 --- /dev/null +++ b/test/support/src/archive_test_helpers.h @@ -0,0 +1,133 @@ +// === archive_test_helpers.h ========================================================================================== +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +#ifndef SEN_TEST_SUPPORT_ARCHIVE_TEST_HELPERS_H +#define SEN_TEST_SUPPORT_ARCHIVE_TEST_HELPERS_H + +// sen +#include "sen/core/base/compiler_macros.h" +#include "sen/core/base/timestamp.h" +#include "sen/core/base/uuid.h" +#include "sen/core/meta/class_type.h" +#include "sen/core/meta/type.h" +#include "sen/db/output.h" + +// generated code +#include "stl/sen/db/basic_types.stl.h" + +// std +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sen::test +{ + +/// Creates a unique temporary directory and deletes it on destruction. +class TempDir +{ +public: + explicit TempDir(std::string prefix = "sen_test_") + : path_(std::filesystem::temp_directory_path() / (std::move(prefix) + getRandomPathPostFix())) + { + std::filesystem::create_directories(path_); + } + + SEN_NOCOPY_NOMOVE(TempDir) + + ~TempDir() + { + std::error_code errorCode; + std::filesystem::remove_all(path_, errorCode); + } + + /// Returns the absolute path to the generated directory. + [[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; } + +private: + /// Returns a random post fix for temporary files paths. + static std::string getRandomPathPostFix() { return sen::UuidRandomGenerator()().toString(); } + +private: + std::filesystem::path path_; +}; + +/// Creates a TimeStamp from the specified number of seconds. +[[nodiscard]] inline sen::TimeStamp makeTime(int64_t seconds) { return sen::TimeStamp(std::chrono::seconds(seconds)); } + +/// Generates output settings to write an archive using the path folder. +[[nodiscard]] inline sen::db::OutSettings makeArchiveSettings(std::string_view name, + const std::filesystem::path& folder, + bool indexKeyframes = true) +{ + sen::db::OutSettings settings; + settings.name = std::string(name); + settings.folder = folder.string(); + settings.indexKeyframes = indexKeyframes; + return settings; +} + +/// Generates output settings to write an archive using a TempDir object. +[[nodiscard]] inline sen::db::OutSettings makeArchiveSettings(std::string_view name, + const TempDir& tempDir, + bool indexKeyframes = true) +{ + return makeArchiveSettings(name, tempDir.path(), indexKeyframes); +} + +/// Constructs the full archive path by appending the archive name to the base folder. +[[nodiscard]] inline std::filesystem::path makeArchivePath(std::string_view name, const std::filesystem::path& folder) +{ + return folder / std::string(name); +} + +/// Constructs the archive path within a TempDir. +[[nodiscard]] inline std::filesystem::path makeArchivePath(std::string_view name, const TempDir& tempDir) +{ + return makeArchivePath(name, tempDir.path()); +} + +/// Creates object metadata for database registration, assigning a session and bus. +template +[[nodiscard]] inline sen::db::ObjectInfo makeObjectInfo(ObjectType* object, + std::string_view session = "test_session", + std::string_view bus = "test_bus") +{ + return sen::db::ObjectInfo {object, std::string(session), std::string(bus)}; +} + +/// Creates object metadata for database registration accepting a std::shared_ptr. +template +[[nodiscard]] inline sen::db::ObjectInfo makeObjectInfo(const std::shared_ptr& object, + std::string_view session = "test_session", + std::string_view bus = "test_bus") +{ + return makeObjectInfo(object.get(), session, bus); +} + +/// Retrieves the MemberHash id of the first property defined in the object's class. +template +[[nodiscard]] inline sen::MemberHash firstPropertyId(const ObjectType& object) +{ + return object.getClass()->getProperties(sen::ClassType::SearchMode::includeParents).front()->getId(); +} + +/// Retrieves the MemberHash id of the first event defined in the object's class. +template +[[nodiscard]] inline sen::MemberHash firstEventId(const ObjectType& object) +{ + return object.getClass()->getEvents(sen::ClassType::SearchMode::includeParents).front()->getId(); +} + +} // namespace sen::test + +#endif // SEN_TEST_SUPPORT_ARCHIVE_TEST_HELPERS_H From 81886a09f62aa569fadf6382b721c9ad6b9e3745 Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Wed, 13 May 2026 07:43:31 +0200 Subject: [PATCH 02/17] test(core): add tests to cover missing gaps of core/meta/property and move implementation checks to the test file resolves SEN-1635 --- libs/core/src/meta/property.cpp | 92 ++------------------------- libs/core/test/meta/property_test.cpp | 37 +++++++++++ 2 files changed, 42 insertions(+), 87 deletions(-) diff --git a/libs/core/src/meta/property.cpp b/libs/core/src/meta/property.cpp index 3cfd2912..11f03e6f 100644 --- a/libs/core/src/meta/property.cpp +++ b/libs/core/src/meta/property.cpp @@ -13,7 +13,6 @@ // sen #include "sen/core/base/assert.h" #include "sen/core/base/hash32.h" -#include "sen/core/base/span.h" #include "sen/core/meta/callable.h" #include "sen/core/meta/event.h" #include "sen/core/meta/method.h" @@ -49,90 +48,14 @@ std::string makeExplanation(const PropertySpec& spec, std::string_view message) sen::throwRuntimeError(makeExplanation(spec, message)); } -/// Throws if the property does not have all the required fields -void checkRequiredFields(const PropertySpec& spec) { impl::checkMemberName(spec.name); } - -/// Throws if the getter method is not valid -void checkGetterMethod(const PropertySpec& spec, const Method& getter) -{ - // getters shall have no arguments - if (!getter.getArgs().empty()) - { - throwError(spec, "getter method takes arguments"); - } - - // getters shall be constant - if (getter.getConstness() != Constness::constant) - { - throwError(spec, "getter method is not constant"); - } - - // getters shall return something - if (getter.getReturnType()->isVoidType()) - { - throwError(spec, "getter method does not return anything"); - } - - // getters shall return the same type as the property - if (getter.getReturnType() != spec.type) - { - throwError(spec, "getter method returns a different type"); - } - - // getters naming shall be consistent with our expectations - if (getter.getName() != Property::makeGetterMethodName(spec)) - { - throwError(spec, "getter method has an invalid name"); - } -} - -/// Throws if the setter method is not valid -void checkSetterMethod(const PropertySpec& spec, const Method& setter) +/// Throws if the property does not have all the required fields or has invalid types +void checkRequiredFields(const PropertySpec& spec) { - // setters shall have only one argument - if (setter.getArgs().size() != 1U) - { - throwError(spec, "setter does not have one argument"); - } - - // setters shall be non-constant - if (setter.getConstness() == Constness::constant) - { - throwError(spec, "setter method is constant"); - } - - // setters shall not return - if (!setter.getReturnType()->isVoidType()) - { - throwError(spec, "setter method returns something"); - } - - // setters shall take the same type as the property - if (setter.getArgs()[0U].type != spec.type) - { - throwError(spec, "setter method takes a different type"); - } - - // setters naming shall be consistent with our expectations - if (setter.getName() != Property::makeSetterMethodName(spec)) - { - throwError(spec, "setter method has an invalid name"); - } -} - -/// Throws if the change notification event is not valid -void checkChangeNotificationEvent(const PropertySpec& spec, const Event& changeEvent) -{ - // change events shall have no arguments - if (!changeEvent.getArgs().empty()) - { - throwError(spec, "change event has arguments"); - } + impl::checkMemberName(spec.name); - // event naming shall be consistent with our expectations - if (changeEvent.getName() != Property::makeChangeNotificationEventName(spec)) + if (spec.type->isVoidType()) { - throwError(spec, "change event has an invalid name"); + throwError(spec, "property cannot be of void type"); } } @@ -180,11 +103,6 @@ Property::Property(PropertySpec spec, Private /*priv*/) changeEventSpec.callableSpec.transportMode = spec_.transportMode; changeEvent_ = Event::make(changeEventSpec); } - - // just to be safe - checkGetterMethod(spec_, *getterMethod_); // check the getter method - checkSetterMethod(spec_, *setterMethod_); // check the setter method - checkChangeNotificationEvent(spec_, *changeEvent_); // check the event } std::shared_ptr Property::make(PropertySpec spec) diff --git a/libs/core/test/meta/property_test.cpp b/libs/core/test/meta/property_test.cpp index 688dea6c..0abc426f 100644 --- a/libs/core/test/meta/property_test.cpp +++ b/libs/core/test/meta/property_test.cpp @@ -400,3 +400,40 @@ TEST(Property, setter) EXPECT_EQ(result, "setNextEventSetter"); } } + +/// @test +/// Checks rejection of void type property +/// @requirements(SEN-355) +TEST(Property, voidTypeProperty) +{ + auto spec = getValidSpec(); + spec.type = sen::VoidType::get(); + EXPECT_THROW(std::ignore = Property::make(spec), std::exception); +} + +/// @test +/// Checks the internal structure, arguments, and return types of the generated getter, setter, and event +/// @requirements(SEN-355) +TEST(Property, internalGenerators) +{ + const auto& spec = getValidSpec(); + const auto instance = Property::make(spec); + + const auto& getter = instance->getGetterMethod(); + EXPECT_TRUE(getter.getArgs().empty()); + EXPECT_EQ(getter.getConstness(), Constness::constant); + EXPECT_FALSE(getter.getReturnType()->isVoidType()); + EXPECT_EQ(getter.getReturnType(), spec.type); + EXPECT_EQ(getter.getName(), Property::makeGetterMethodName(spec)); + + const auto& setter = instance->getSetterMethod(); + EXPECT_EQ(setter.getArgs().size(), 1U); + EXPECT_EQ(setter.getConstness(), Constness::nonConstant); + EXPECT_TRUE(setter.getReturnType()->isVoidType()); + EXPECT_EQ(setter.getArgs()[0U].type, spec.type); + EXPECT_EQ(setter.getName(), Property::makeSetterMethodName(spec)); + + const auto& changeEvent = instance->getChangeEvent(); + EXPECT_TRUE(changeEvent.getArgs().empty()); + EXPECT_EQ(changeEvent.getName(), Property::makeChangeNotificationEventName(spec)); +} From c6b78f8438d72be119ac3a58ee4679a6634987c6 Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Wed, 13 May 2026 13:41:27 +0200 Subject: [PATCH 03/17] feat(core): allow structures, enumerations, and variants to end with comment lines and commas after the last field resolves SEN-1645 --- libs/core/src/lang/stl_parser.cpp | 42 +++++++++ libs/core/test/lang/parser_test.cpp | 128 ++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) diff --git a/libs/core/src/lang/stl_parser.cpp b/libs/core/src/lang/stl_parser.cpp index 6333169c..84921ab1 100644 --- a/libs/core/src/lang/stl_parser.cpp +++ b/libs/core/src/lang/stl_parser.cpp @@ -194,6 +194,17 @@ StlStructStatement StlParser::structDeclaration() while (!check(StlTokenType::rightBrace) && !isAtEnd()) { + previousComment_ = {}; + while (check(StlTokenType::comment)) + { + previousComment_.push_back(consume(StlTokenType::comment, {"expecting comment"})); + } + + if (!previousComment_.empty() && (check(StlTokenType::rightBrace) || isAtEnd())) + { + break; + } + statement.fields.push_back(structFieldStatement()); } @@ -207,6 +218,9 @@ StlStructFieldStatement StlParser::structFieldStatement() StlStructFieldStatement statement; + statement.description = previousComment_; + previousComment_.clear(); + // maybe we have a comment before the field while (check(StlTokenType::comment)) { @@ -270,6 +284,17 @@ StlEnumStatement StlParser::enumDeclaration() while (!check(StlTokenType::rightBrace) && !isAtEnd()) { + previousComment_ = {}; + while (check(StlTokenType::comment)) + { + previousComment_.push_back(consume(StlTokenType::comment, {"expecting comment"})); + } + + if (!previousComment_.empty() && (check(StlTokenType::rightBrace) || isAtEnd())) + { + break; + } + statement.enumerators.push_back(enumeratorDeclaration()); // If next one is a comment and not a comma, we should be at the end of the enumeration @@ -307,6 +332,9 @@ StlEnumeratorStatement StlParser::enumeratorDeclaration() StlEnumeratorStatement statement; + statement.description = previousComment_; + previousComment_.clear(); + // maybe we have a comment before the enumerator while (check(StlTokenType::comment)) { @@ -450,6 +478,17 @@ StlVariantStatement StlParser::variantDeclaration() while (!check(StlTokenType::rightBrace) && !isAtEnd()) { + previousComment_ = {}; + while (check(StlTokenType::comment)) + { + previousComment_.push_back(consume(StlTokenType::comment, {"expecting comment"})); + } + + if (!previousComment_.empty() && (check(StlTokenType::rightBrace) || isAtEnd())) + { + break; + } + statement.elements.push_back(variantElementDeclaration()); } @@ -464,6 +503,9 @@ StlVariantElement StlParser::variantElementDeclaration() StlVariantElement statement; + statement.description = previousComment_; + previousComment_.clear(); + // maybe we have a comment before the variant element while (check(StlTokenType::comment)) { diff --git a/libs/core/test/lang/parser_test.cpp b/libs/core/test/lang/parser_test.cpp index 2834621b..7db60d7b 100644 --- a/libs/core/test/lang/parser_test.cpp +++ b/libs/core/test/lang/parser_test.cpp @@ -437,3 +437,131 @@ class Person EXPECT_ANY_THROW(auto statements = parser.parse()); } } + +/// @test +/// Checks that the last field of structs, enums and variants can end with a comma. +/// It also checks that they can have a comment line before the closing brace +/// @requirements(SEN-903) +TEST(Parser, trailingCommasAndFloatingComments) +{ + const std::string program = + R"( +// A struct whose last field has a comma, and which has a line containing a comment before the closing brace +struct CPrint +{ + flags : u32, + color : u32, + text : string, // last element ends with a coma + + // extra : string +} + +// An enum whose last enumerator has a comma, and which has a line containing a comment before the closing brace +enum OsKind : u8 +{ + windowsOs, // Microsoft Windows + linuxOs, // Linux + androidOs, // Android Linux + appleOs, // Apple OS (iOS, tvOS, etc..) + unixOs, // all unices not caught above + posixOs, // Posix + otherOs, // other, unknown + // myFirstOs + // mySecondOs +} + +// A variant whose last element has a comma, and which has a line containing a comment before the closing brace +variant TerminalCommand +{ + HideCursor, + ShowCursor, + SaveCursorPosition, + RestoreCursorPosition, + MoveCursorLeft, + MoveCursorRight, + MoveCursorUp, + MoveCursorDown, + Print, + CPrint, // last element ends with a coma and its description should not include 'customCommand' next comment + +// customCommand +} + + )"; + + StlScanner scanner(program); + const auto tokens = scanner.scanTokens(); + EXPECT_FALSE(tokens.empty()); + + StlParser parser(tokens); + const auto statements = parser.parse(); + + ASSERT_EQ(3U, statements.size()); + + // Struct + ASSERT_TRUE(std::holds_alternative(statements[0])); + const auto& structure = std::get(statements[0]); + ASSERT_EQ(3U, structure.fields.size()); + EXPECT_EQ("flags", structure.fields[0].identifier.lexeme()); + EXPECT_EQ("text", structure.fields[2].identifier.lexeme()); + ASSERT_EQ(0U, structure.fields[1].description.size()); + EXPECT_EQ("last element ends with a coma", structure.fields[2].description[0].lexeme()); + + // Enum + ASSERT_TRUE(std::holds_alternative(statements[1])); + const auto& enumeration = std::get(statements[1]); + ASSERT_EQ(7U, enumeration.enumerators.size()); + EXPECT_EQ("windowsOs", enumeration.enumerators[0].identifier.lexeme()); + EXPECT_EQ("linuxOs", enumeration.enumerators[1].identifier.lexeme()); + EXPECT_EQ("otherOs", enumeration.enumerators[6].identifier.lexeme()); + + // Variant + ASSERT_TRUE(std::holds_alternative(statements[2])); + const auto& variant = std::get(statements[2]); + ASSERT_EQ(10U, variant.elements.size()); + EXPECT_EQ("last element ends with a coma and its description should not include 'customCommand' next comment", + variant.elements[9].description[0].lexeme()); +} + +/// @test +/// Checks that structures, enums and variants can be empty with only a comment inside +/// @requirements(SEN-903) +TEST(Parser, emptyBlocksWithFloatingComments) +{ + const std::string program = + R"( +struct EmptyStruct { + // Test comment +} + +enum EmptyEnum : u8 { + // Test comment +} + +variant EmptyVariant { + // Test comment +} + + )"; + + StlScanner scanner(program); + const auto tokens = scanner.scanTokens(); + EXPECT_FALSE(tokens.empty()); + + StlParser parser(tokens); + const auto statements = parser.parse(); + + ASSERT_EQ(3U, statements.size()); + + // Struct + ASSERT_TRUE(std::holds_alternative(statements[0])); + EXPECT_EQ(0U, std::get(statements[0]).fields.size()); + + // Enum + ASSERT_TRUE(std::holds_alternative(statements[1])); + EXPECT_EQ(0U, std::get(statements[1]).enumerators.size()); + + // Variant + ASSERT_TRUE(std::holds_alternative(statements[2])); + EXPECT_EQ(0U, std::get(statements[2]).elements.size()); +} From 2cfbdc6513576ab1c4b6a7ca87bae79357b11525 Mon Sep 17 00:00:00 2001 From: Gema Aparicio Cantalapiedra Date: Wed, 13 May 2026 16:42:06 +0200 Subject: [PATCH 04/17] feature(docs): object naming convention add to docs Object naming convention added to sen documentation resolves SEN-1553 --- docs/howto_guides/components.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/howto_guides/components.md b/docs/howto_guides/components.md index a8cc259d..c9e01a45 100644 --- a/docs/howto_guides/components.md +++ b/docs/howto_guides/components.md @@ -545,3 +545,7 @@ We just need to be aware of the following: Object names must be unique within the bus in which they are published. Otherwise, an exception will be raised. + +### Object's naming convention + +Sen supports the use of all special characters for published object naming, with the only exception of literal space characters (" "), which are restricted. From ba2e173440692e6b86d40ba0ed66babc3b0007cf Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Fri, 15 May 2026 09:44:54 +0200 Subject: [PATCH 05/17] doc(examples): improve app examples documentation for the web explorer and the python REST client Improve the documentation for the web explorer and REST Python client app examples with usage guides. resolves SEN-1530 --- docs/examples/index.md | 38 +++-- examples/apps/rest_python/readme.md | 7 +- examples/apps/rest_python/sen_client.py | 200 +++++++++++++++++++++++- examples/apps/web_explorer/readme.md | 51 +++++- 4 files changed, 276 insertions(+), 20 deletions(-) diff --git a/docs/examples/index.md b/docs/examples/index.md index 5bc871ed..8e1676fa 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -17,26 +17,36 @@ compile it, and run it under a Sen kernel. The [examples](../examples/index.md) are numbered and ordered by complexity. Follow them in sequence: -| # | Example | What you learn | -|---|---------|---------------| -| 1 | [Calculators](../../examples/config/1_calculators/readme.md) | Basic package, multiple implementations, shell interaction | -| 2 | [Inheritance](../../examples/config/2_inheritance/readme.md) | STL inheritance, template injection | -| 3 | [Aircraft](../../examples/config/3_aircraft/readme.md) | HLA FOMs, `update()` loop, virtual time | -| 4 | [School](../../examples/config/4_school/readme.md) | Object discovery, events, multi-component | -| 6 | [Recorder](../../examples/config/6_recorder/readme.md) | Recording, Python post-processing | -| 7 | [Replayer](../../examples/config/7_replayer/readme.md) | Replay with real-time and stepped execution | -| 8 | [InfluxDB](../../examples/config/8_influx/readme.md) | Grafana visualisation | -| 9 | [HLA Servers](../../examples/config/9_hla_servers/readme.md) | Request/response servers | -| 10 | [Python](../../examples/config/10_python/readme.md) | Embedded Python scripting | -| 11 | [Shapes](../../examples/config/11_shapes/readme.md) | Interest management, Sen Query Language | -| 12 | [Fibonacci](../../examples/config/12_fibonacci/readme.md) | Deferred methods, load balancing | -| 13 | [Timer](../../examples/config/13_timer/readme.md) | Checked properties, state validation | +| # | Example | What you learn | +| --- | ------------------------------------------------------------ | ---------------------------------------------------------- | +| 1 | [Calculators](../../examples/config/1_calculators/readme.md) | Basic package, multiple implementations, shell interaction | +| 2 | [Inheritance](../../examples/config/2_inheritance/readme.md) | STL inheritance, template injection | +| 3 | [Aircraft](../../examples/config/3_aircraft/readme.md) | HLA FOMs, `update()` loop, virtual time | +| 4 | [School](../../examples/config/4_school/readme.md) | Object discovery, events, multi-component | +| 6 | [Recorder](../../examples/config/6_recorder/readme.md) | Recording, Python post-processing | +| 7 | [Replayer](../../examples/config/7_replayer/readme.md) | Replay with real-time and stepped execution | +| 8 | [InfluxDB](../../examples/config/8_influx/readme.md) | Grafana visualisation | +| 9 | [HLA Servers](../../examples/config/9_hla_servers/readme.md) | Request/response servers | +| 10 | [Python](../../examples/config/10_python/readme.md) | Embedded Python scripting | +| 11 | [Shapes](../../examples/config/11_shapes/readme.md) | Interest management, Sen Query Language | +| 12 | [Fibonacci](../../examples/config/12_fibonacci/readme.md) | Deferred methods, load balancing | +| 13 | [Timer](../../examples/config/13_timer/readme.md) | Checked properties, state validation | ### 4. Go deeper with the how-to guides Once you are comfortable with the examples, the [How-To Guides](../howto_guides/objects.md) cover specific topics in depth: working with objects, generated code, logging, dead reckoning, and more. +## Example applications + +Included in the same [examples](../examples/) directory, you can find a set of example applications: + +| Application | Description | +| -------------------------------------------------------------- | ---------------------------------------- | +| [Web explorer](../../examples/apps/web_explorer/readme.md) | Basic Sen explorer for the web browser | +| [REST Python](../../examples/apps/rest_python/readme.md) | Python client for the Sen REST component | +| [Recording inspector](../../examples/apps/recording_inspector) | | + ## Reference material - [STL language reference](../users_guide/stl.md) - full syntax of the Sen Type Language diff --git a/examples/apps/rest_python/readme.md b/examples/apps/rest_python/readme.md index e2ff1946..f6f137e6 100644 --- a/examples/apps/rest_python/readme.md +++ b/examples/apps/rest_python/readme.md @@ -29,8 +29,8 @@ Prepare the Python environment ```bash python3 -m venv .env -source env/bin/activate -pip install -r requirements +source .env/bin/activate +pip install -r requirements.txt ``` Now run the client example: @@ -38,3 +38,6 @@ Now run the client example: ```bash python sen_client.py ``` + +This example works as a simple python client for the Sen REST component. It instantiates a SenClient object that starts +a session, enable notifications and does some basic operations to exemplify how it can be used. diff --git a/examples/apps/rest_python/sen_client.py b/examples/apps/rest_python/sen_client.py index 4228a9a7..e3cb6543 100644 --- a/examples/apps/rest_python/sen_client.py +++ b/examples/apps/rest_python/sen_client.py @@ -5,6 +5,11 @@ # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""This program contains an example of how users can interact with the REST API using Python. + +It shows how to sign in, create interests, display details and interact with remote objects. +""" + import requests import json import sseclient @@ -13,7 +18,17 @@ from typing import List, Dict, Any class SenClient: - def __init__(self, base_url = "http://localhost"): + """ + Represents the client for the Sen REST component. + + Attributes: + base_url (str): URL of the Sen REST server. + session (requests.Session): Active HTTP session. + sse_event_types (list): List of notifiable event types. + token (str): Token needed for authentication. + """ + + def __init__(self, base_url="http://localhost"): self.base_url = base_url.rstrip('/') self.session = requests.Session() self.sse_event_types = [ @@ -25,7 +40,12 @@ def __init__(self, base_url = "http://localhost"): ] self.token = None - def get_headers(self, is_sse = False): + def get_headers(self, is_sse=False): + """Gets the headers needed for REST API requests. + + Args: + is_sse (bool): Whether notifications are enabled. + """ header = {"Content-Type": "application/json", } if is_sse: header["Accept"] = "text/event-stream" @@ -37,6 +57,17 @@ def get_headers(self, is_sse = False): return header def get(self, href): + """Makes GET request to the REST API. + + Args: + href (str): Endpoint route. + + Returns: + dict: Response of the request. + + Raises: + requests.RequestException: If there is any error in the request. + """ try: url = href if href.startswith('http') else f"{self.base_url}{href}" response = self.session.get( @@ -51,6 +82,17 @@ def get(self, href): raise def post(self, href): + """Makes POST request to the REST API. + + Args: + href (str): Endpoint route. + + Returns: + dict: Response of the request. + + Raises: + requests.RequestException: If there is any error in the request. + """ try: url = href if href.startswith('http') else f"{self.base_url}{href}" response = self.session.post( @@ -65,11 +107,23 @@ def post(self, href): raise def post_json(self, href, body): + """Makes POST request to the REST API with a body attached. + + Args: + href (str): Endpoint route. + body (str): Body of the request. + + Returns: + dict: Response of the request. + + Raises: + requests.RequestException: If there is any error in the request. + """ try: url = href if href.startswith('http') else f"{self.base_url}{href}" response = self.session.post( url, - headers= self.get_headers(), + headers=self.get_headers(), json=body ) response.raise_for_status() @@ -80,6 +134,18 @@ def post_json(self, href, body): raise def post_args(self, href, args_str): + """Makes POST request to the REST API when some arguments are needed. + + Args: + href (str): Endpoint route. + args_str (str): Arguments sent to the endpoint. + + Returns: + dict: Response of the request. + + Raises: + requests.RequestException: If there is any error in the request. + """ args_str = args_str.strip() if args_str: @@ -97,9 +163,15 @@ def post_args(self, href, args_str): return data def enable_notifications(self): + """Enable notifications that are displayed on standard output.""" import threading def listen_sse(): + """Tries to enable notifications and displays them on the standard output. + + Raises: + Exception: If there is any error in the request. + """ try: notifications = sseclient.SSEClient(f"{self.base_url}/api/sse", headers=self.get_headers(True)) for notification in notifications: @@ -114,17 +186,37 @@ def listen_sse(): print("Notifications enabled") def disable_notifications(self): + """Disable notifications if they are enabled""" if self.sse_thread: self.sse_thread = None print("SSE disabled") def signin(self, client_id): + """Sign in to the REST API to get an API token. + + Args: + client_id (str): Client id. + + Returns: + dict: Response containing the API token. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.post_json("/api/auth", {"id": client_id}) print("Signed in successfully") self.token = data["token"] return data def start_client_session(self, client_id): + """Start a client session and get all the sessions if a client id is provided. + + Args: + client_id (str): Client id. + + Raises: + requests.RequestException: If there is any error in the request. + """ if not client_id: print("Client ID is required") return @@ -134,34 +226,89 @@ def start_client_session(self, client_id): self.get_sessions() def get_sessions(self): + """Get all the sessions. + + Returns: + list: List containing all the sessions. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.get("/api/sessions") return data def print_sessions(self, sessions): + """Display all the sessions on standard output. + + Args: + sessions (list): List with all the sessions. + """ print("\n=== Sessions ===") for session in sessions: print(f" {session}") print() def create_interest(self, name, query): + """Create an interest based on a query. + + Args: + name (str): Name of the interest. + query (str): Query used to create the interest. + + Returns: + dict: Dictionary containing the created interest name. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.post_json("/api/interests", {"name": name, "query": query}) return data def reload_interests(self): + """Get all the interests created. + + Returns: + list[dict]: List containing all the interests with their name, bus and session. + + Raises: + requests.RequestException: If there is any error in the request. + """ interests = self.get("/api/interests") return interests def print_interests(self, interests): + """Display all the interests on standard output. + + Args: + interests (list[dict]): List containing all the interests. + """ print("\n=== Interests ===") for interest in interests: print(f" {json.dumps(interest)}") print() def get_objects(self, interest_name): + """Get all the objects contained on an interest. + + Args: + interest_name (str): Name of the interest. + + Returns: + list[dict]: List containing all the objects with their class name, link, local name, + name and object id. + + Raises: + requests.RequestException: If there is any error in the request. + """ objects = self.get(f"/api/interests/{interest_name}/objects") return objects def print_objects(self, objects): + """Display all the objects contained on an interest on standard output. + + Args: + interests (list[dict]): List containing all the objects. + """ print("\n=== Objects ===") for obj in objects: print(f" {obj.get('name')} [ {obj.get('localname')} ]") @@ -169,10 +316,26 @@ def print_objects(self, objects): print() def get_object(self, interest_name, object_name): + """Get all the object details. + + Args: + object_name (str): Name of the object. + + Returns: + dict: Dictionary with all the object with its class name, description, links, local name, name and object id. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.get(f"/api/interests/{interest_name}/objects/{object_name}") return data def print_object(self, data): + """Display all the object details. + + Args: + data (dict): Dictionary containing all the object details. + """ methods = [] properties = [] events = [] @@ -210,13 +373,44 @@ def print_object(self, data): print() def get_method_info(self, interest_name, object_name, method): + """Get all the method details. + + Args: + interest_name (str): Name of the interest. + object_name (str): Name of the object. + method (str): Name of the method. + + Returns: + dict: Dictionary containing all the method arguments, description, name and return type of the result. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.get(f"/api/interests/{interest_name}/objects/{object_name}/methods/{method}") return data def print_method_def(self, method_def): + """Display all the method details. + + Args: + method_def (dict): Dictionary containing all the method details. + """ print(f"Method definition: {method_def}") def invoke_method(self, interest_name, object_name, method): + """Invoke a method. + + Args: + interest_name (str): Name of the interest. + object_name (str): Name of the object. + method (str): Name of the method. + + Returns: + dict: Dictionary with the method invocation id and its state. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.post_args(f"/api/interests/{interest_name}/objects/{object_name}/methods/{method}/invoke", "") return data diff --git a/examples/apps/web_explorer/readme.md b/examples/apps/web_explorer/readme.md index 23ff3910..b997edd9 100644 --- a/examples/apps/web_explorer/readme.md +++ b/examples/apps/web_explorer/readme.md @@ -5,11 +5,16 @@ real-time updates through Server-Sent Events (SSE). ## Prerequisites -- Docker +--- + +- Docker engine - make sure to follow the [installation guide](https://docs.docker.com/engine/install/) + and the [post-installation steps](https://docs.docker.com/engine/install/linux-postinstall/) - A running Sen REST component ## Example structure +--- + ``` web_explorer/ ├── index.html # Web page @@ -21,6 +26,8 @@ web_explorer/ ## How to run +--- + Create a **sen** configuration file (e.g. config.yaml) to load the REST component: ```yaml @@ -49,3 +56,45 @@ Open the browser (http://localhost:8000) and developer console to see: - SSE event streams - Error messages - Object interaction logs + +## Usage + +### Start session + +First of all, start session with the Client ID of choice and click `Start Session`. +This step is needed in order to interact with the component. + +Next to `Start Session` button there is a checkbox with the name `SSE`. This checkbox enables the browser to receive +push notifications from the REST component when Sen objects are updated. + +### Create interest + +To create an interest, fill both fields in the `Interest` section. The first field corresponds to the interest name and +the second field is for the interest query. The interest query must be written on Sen Query Language. There is a section +called `Sessions` which shows all the available sessions. + +Interests are saved until the Sen REST component is ended. To show all the created interests, click `Reload` on the +`Available Interests` section. + +NOTE: To show newly created interests after `Reload` has already been clicked, refresh the page and click `Reload` +again. + +### Retrieve objects + +Once an interest is created, click `Retrieve objects` in the `Available Interests` section to show all the objects in +that interest. + +The `Objects` section is where object elements will be shown. These include methods, properties and events. To do that, +click +`Get Object`. This will show all the object elements classified by type. + +For each type of element, different options are available: + +- In `Object Methods` you can get every method definition and invoke them. + You can do that by clicking `Definition` + and `Invoke` buttons respectively. For invoking a method, the arguments must be defined in the `Invoke Method Args` + section. +- In `Object Properties` you can get every property value and subscribe or unsubscribe to them. To do that, click + `Get Value`, `Subscribe` and `Unsubscribe` buttons respectively. +- In `Object Events` you can subscribe or unsubscribe to an event by clicking `Subscribe` and `Unsubscribe` buttons + respectively. From c546c139481f4a7c48d5de600556dd1e03e0228b Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Mon, 18 May 2026 09:23:49 +0200 Subject: [PATCH 06/17] feat(rest): optionally include property values on the object retrieval handler Add query param to the object retrieval handler to include property values resolves SEN-1593 --- components/rest/src/sen_router.cpp | 47 +++++++++++-------- components/rest/src/sen_router.h | 4 +- components/rest/test/e2e_test.cpp | 73 +++++++++++++++++++++++++++++- docs/components/openapi.yaml | 9 ++++ 4 files changed, 111 insertions(+), 22 deletions(-) diff --git a/components/rest/src/sen_router.cpp b/components/rest/src/sen_router.cpp index a4ebf685..0c980573 100644 --- a/components/rest/src/sen_router.cpp +++ b/components/rest/src/sen_router.cpp @@ -17,6 +17,7 @@ #include "locators.h" #include "notification_loop.h" #include "object_interests_manager.h" +#include "response_adapter.h" #include "types.h" #include "utils.h" @@ -156,18 +157,9 @@ std::shared_ptr SenRouter::getObject(const InterestSubscription& in return *objectIt; } -std::optional SenRouter::getObjectDefinition(const InterestSubscription& interestSubscription, - const std::string& urlPath, - const std::string& objectName) const +Object SenRouter::getObjectDefinition(const std::string& urlPath, const sen::Object& object) const { - auto object = getObject(interestSubscription, objectName); - if (!object) - { - return std::nullopt; - } - - auto objectClass = object->getClass(); - + auto objectClass = object.getClass(); Links links; for (const auto& method: objectClass->getMethods(sen::ClassType::SearchMode::includeParents)) { @@ -202,10 +194,10 @@ std::optional SenRouter::getObjectDefinition(const InterestSubscription& } return Object { - object->getId().get(), - object->getName(), + object.getId().get(), + object.getName(), std::string(objectClass->getQualifiedName()), - object->getLocalName(), + object.getLocalName(), std::string(objectClass->getDescription()), links, }; @@ -381,7 +373,7 @@ SenRouter::SenRouter(kernel::RunApi& api): api_(api) addStreamRoute( HttpMethod::httpGet, "/api/sse", bindAuthStreamRouteCallback(this, &SenRouter::getNotificationsHandler)); - // types instrospection + // types introspection addRoute(HttpMethod::httpGet, "/api/types/:type", bindAuthRouteCallback(this, &SenRouter::getTypeIntrospection)); } @@ -614,13 +606,32 @@ JsonResponse SenRouter::getObjectHandler(ClientSession& clientSession, } const auto& interestSubscription = interestSubscriptionRes.getValue(); - auto object = getObjectDefinition(interestSubscription, httpSession.getRequest().path(), urlParams[1]); - if (!object) + auto objectReference = getObject(interestSubscription, urlParams[1]); + + if (!objectReference) { return getErrorNotFound(); } - return JsonResponse {httpSuccess, *object}; + auto object = getObjectDefinition(httpSession.getRequest().path(), *objectReference); + auto var = sen::toVariant(object); + adaptForJsonResponse(var, sen::MetaTypeTrait::meta().type()); + auto jsonObject = nlohmann::json::parse(toJson(var)); + + if (!queryParams.empty() && queryParams.find("includeValues")->second == "true") + { + nlohmann::json values; + + for (const auto& prop: objectReference->getClass()->getProperties(sen::ClassType::SearchMode::includeParents)) + { + auto value = objectReference->getPropertyUntyped(prop.get()); + values[prop->getName()] = nlohmann::json::parse(toJson(value)); + } + + jsonObject["properties"] = values; + } + + return JsonResponse {httpSuccess, jsonObject.dump()}; } JsonResponse SenRouter::getPropertyHandler(ClientSession& clientSession, diff --git a/components/rest/src/sen_router.h b/components/rest/src/sen_router.h index 0eb9f244..a73ebe60 100644 --- a/components/rest/src/sen_router.h +++ b/components/rest/src/sen_router.h @@ -185,9 +185,7 @@ class SenRouter: public BaseRouter [[nodiscard]] std::shared_ptr getObject(const InterestSubscription& interestSubscription, const std::string& objectName) const; - [[nodiscard]] std::optional getObjectDefinition(const InterestSubscription& interestSubscription, - const std::string& urlPath, - const std::string& objectName) const; + [[nodiscard]] Object getObjectDefinition(const std::string& urlPath, const sen::Object& object) const; [[nodiscard]] std::optional getMethodDefinition(const InterestSubscription& interestSubscription, const MethodLocator& methodLocator) const; [[nodiscard]] std::optional getEventDefinition(const InterestSubscription& interestSubscription, diff --git a/components/rest/test/e2e_test.cpp b/components/rest/test/e2e_test.cpp index f7a984dd..bc3688ac 100644 --- a/components/rest/test/e2e_test.cpp +++ b/components/rest/test/e2e_test.cpp @@ -432,7 +432,7 @@ TEST(Rest, e2e_get_existing_object) } /// @test -/// End-to-end test for getting an non-existing object +/// End-to-end test for getting a non-existing object /// @requirements(SEN-1061) TEST(Rest, e2e_get_non_existing_object) { @@ -461,6 +461,77 @@ TEST(Rest, e2e_get_non_existing_object) ASSERT_EQ(ret.statusCode, 404); } +/// @test +/// End-to-end test getting existing object with its properties when optional query param is true +TEST(Rest, e2e_get_existing_object_including_properties) +{ + Server server; + + // Authenticate + auto token = authenticate(); + ASSERT_TRUE(token.has_value()); + + // Create interest + auto createRet = request(HttpMethod::httpPost, + "127.0.0.1", + "12345", + "/api/interests", + Json {{"name", "test_interest"}, {"query", "SELECT * FROM local.kernel"}}, + token.value()); + ASSERT_EQ(createRet.statusCode, 200); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Try to get an object properties + auto ret = request(HttpMethod::httpGet, + "127.0.0.1", + "12345", + "/api/interests/test_interest/objects/api?includeValues=true", + Json(), + token.value()); + ASSERT_EQ(ret.statusCode, 200); + + auto interests = Json::parse(ret.body); + ASSERT_TRUE(interests.is_object()); + ASSERT_TRUE(interests.contains("properties")); + ASSERT_TRUE(interests["properties"].is_object()); +} + +/// @test +/// End-to-end test getting existing object without its properties when optional query param is not true +TEST(Rest, e2e_get_existing_object_not_including_properties) +{ + Server server; + + // Authenticate + auto token = authenticate(); + ASSERT_TRUE(token.has_value()); + + // Create interest + auto createRet = request(HttpMethod::httpPost, + "127.0.0.1", + "12345", + "/api/interests", + Json {{"name", "test_interest"}, {"query", "SELECT * FROM local.kernel"}}, + token.value()); + ASSERT_EQ(createRet.statusCode, 200); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Try to not get an object properties + auto ret = request(HttpMethod::httpGet, + "127.0.0.1", + "12345", + "/api/interests/test_interest/objects/api?includeValues=false", + Json(), + token.value()); + ASSERT_EQ(ret.statusCode, 200); + + auto interests = Json::parse(ret.body); + ASSERT_TRUE(interests.is_object()); + ASSERT_FALSE(interests.contains("properties")); +} + /// @test /// End-to-end test for getting a method definition /// @requirements(SEN-1061) diff --git a/docs/components/openapi.yaml b/docs/components/openapi.yaml index 57452739..222f2cc7 100644 --- a/docs/components/openapi.yaml +++ b/docs/components/openapi.yaml @@ -227,6 +227,12 @@ paths: required: true schema: type: string + - in: query + name: includeValues + description: Whether to include property values + required: false + schema: + type: string get: summary: Get object details description: Returns information about a specific object including available @@ -783,6 +789,9 @@ components: items: $ref: '#/components/schemas/Link' description: Links to available methods, properties, and events + properties: + type: object + description: Properties with its values required: - objectId - name From e7d6088f61f386455fa96c169c2628d2183e87f8 Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Tue, 19 May 2026 12:42:17 +0200 Subject: [PATCH 07/17] fix(rest): OpenAPI spec vs implementation discrepancies Reported discrepancies between implementation and specification. resolves SEN-1590 --- components/rest/src/response_adapter.cpp | 2 +- components/rest/stl/types.stl | 8 ++++---- components/rest/test/e2e_test.cpp | 2 +- docs/components/openapi.yaml | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/components/rest/src/response_adapter.cpp b/components/rest/src/response_adapter.cpp index 2f8a8416..c390c855 100644 --- a/components/rest/src/response_adapter.cpp +++ b/components/rest/src/response_adapter.cpp @@ -83,7 +83,7 @@ void adaptForJsonResponse(sen::Var& var, const sen::Type* type) switch (var_.getCopyAs()) { case 0: - var_ = "event"; + var_ = "evt"; break; case 1: diff --git a/components/rest/stl/types.stl b/components/rest/stl/types.stl index 542dae00..f11b0ae9 100644 --- a/components/rest/stl/types.stl +++ b/components/rest/stl/types.stl @@ -86,8 +86,8 @@ struct ObjectSummary { objectId : u32, name : string, - classname : string, - localname : string, + className : string, + localName : string, link : Link } @@ -127,8 +127,8 @@ struct Object { objectId : u32, name : string, - classname : string, - localname : string, + className : string, + localName : string, description : string, links : Links } diff --git a/components/rest/test/e2e_test.cpp b/components/rest/test/e2e_test.cpp index bc3688ac..a06112b0 100644 --- a/components/rest/test/e2e_test.cpp +++ b/components/rest/test/e2e_test.cpp @@ -428,7 +428,7 @@ TEST(Rest, e2e_get_existing_object) auto interests = Json::parse(ret.body); ASSERT_TRUE(interests.is_object()); - ASSERT_EQ(interests["localname"], "rest.local.kernel.api"); + ASSERT_EQ(interests["localName"], "rest.local.kernel.api"); } /// @test diff --git a/docs/components/openapi.yaml b/docs/components/openapi.yaml index 222f2cc7..02e5a4e9 100644 --- a/docs/components/openapi.yaml +++ b/docs/components/openapi.yaml @@ -820,8 +820,8 @@ components: method: type: string enum: - - httpGet - - httpPost + - get + - post description: HTTP method to use required: - rel @@ -836,7 +836,7 @@ components: description: type: string description: Method description - ret_type: + retType: type: string description: Return type name args: @@ -847,7 +847,7 @@ components: required: - name - description - - ret_type + - retType - args Argument: type: object @@ -906,10 +906,10 @@ components: type: string enum: - evt - - property - invoke - - objectAdded - - objectRemoved + - property + - object_added + - object_removed description: Notification type data: type: object From 4eabee1ed4da0b87b5d17b3801d77679979eb038 Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Tue, 19 May 2026 13:45:19 +0200 Subject: [PATCH 08/17] fix(rest): crash on malformed create interest request resolves SEN-1646 --- components/rest/src/sen_router.cpp | 4 ++-- components/rest/test/e2e_test.cpp | 14 ++++++++++++++ components/rest/test/request.cpp | 10 ++++++---- components/rest/test/request.h | 2 +- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/components/rest/src/sen_router.cpp b/components/rest/src/sen_router.cpp index 0c980573..8c1cf3e6 100644 --- a/components/rest/src/sen_router.cpp +++ b/components/rest/src/sen_router.cpp @@ -486,10 +486,10 @@ JsonResponse SenRouter::createInterestHandler(ClientSession& clientSession, { logClientSession(clientSession, "createInterest"); - const auto payload = Json::parse(httpSession.getRequest().body()); - try { + const auto payload = Json::parse(httpSession.getRequest().body()); + Interest interest; interest.query = payload.at("query").get(); interest.name = payload.at("name").get(); diff --git a/components/rest/test/e2e_test.cpp b/components/rest/test/e2e_test.cpp index a06112b0..0bf53d8c 100644 --- a/components/rest/test/e2e_test.cpp +++ b/components/rest/test/e2e_test.cpp @@ -233,6 +233,20 @@ TEST(Rest, e2e_create_interest_invalid_query) ASSERT_EQ(ret.statusCode, 400); } +/// @test +/// End-to-end test for malformed request to create interest +/// @requirements(SEN-1061) +TEST(Rest, e2e_create_interest_malformed_request) +{ + Server server; + + auto token = authenticate(); + ASSERT_TRUE(token.has_value()); + + auto ret = request(HttpMethod::httpPost, "127.0.0.1", "12345", "/api/interests", std::nullopt, token.value()); + ASSERT_EQ(ret.statusCode, 400); +} + /// @test /// End-to-end test for getting an existing interest /// @requirements(SEN-1061) diff --git a/components/rest/test/request.cpp b/components/rest/test/request.cpp index 13b76642..ebbb61a9 100644 --- a/components/rest/test/request.cpp +++ b/components/rest/test/request.cpp @@ -41,12 +41,11 @@ HttpResponse request(const HttpMethod& method, const std::string& host, const std::string& port, const std::string& path, - const Json& data, + const std::optional data, const std::string& token, bool isSSE) { HttpResponse result {0, ""}; - asio::io_context context; asio::ip::tcp::resolver resolver(context); asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, port); @@ -61,8 +60,11 @@ HttpResponse request(const HttpMethod& method, { case HttpMethod::httpPost: request = "POST"; - payload = data.dump(); - payloadSize = payload.size(); + if (data.has_value()) + { + payload = data->dump(); + payloadSize = payload.size(); + } break; case HttpMethod::httpDelete: request = "DELETE"; diff --git a/components/rest/test/request.h b/components/rest/test/request.h index 333445ea..2215cce2 100644 --- a/components/rest/test/request.h +++ b/components/rest/test/request.h @@ -31,7 +31,7 @@ HttpResponse request(const HttpMethod& method, const std::string& host, const std::string& port, const std::string& path, - const Json& data = Json(), + const std::optional data = Json(), const std::string& token = "", bool isSSE = false); From 3422f5630e713f6e9e50fee1dae0bbcca4f8df30 Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Tue, 19 May 2026 15:34:52 +0200 Subject: [PATCH 09/17] build(cmake)!: respect global cmake output directories if defined BREAKING CHANGE: Targets will now be generated in the defined directories if global CMake output directories are set. Projects that have these directories defined, but still hardcode paths assuming that Sen targets will be placed in `${PROJECT_BINARY_DIR}/bin` or `lib`, will break. Users must update their build systems to reference the appropriate CMake variables instead of hardcoded paths. resolves SEN-1654 --- .../package/my_package/CMakeLists.txt | 1 - apps/cli_gen/test/CMakeLists.txt | 2 +- cmake/util/sen_misc_utils.cmake | 20 +++++++++++++------ cmake/util/test.cmake | 19 +++++++++--------- components/py/test/CMakeLists.txt | 2 +- .../integration/crash_report/CMakeLists.txt | 3 +-- .../runtime_compatibility/CMakeLists.txt | 12 +++-------- .../test/integration/transport/CMakeLists.txt | 4 +--- .../integration/type_clash/CMakeLists.txt | 16 +++++---------- test/util/chaos_monkeys/CMakeLists.txt | 1 - test/util/inheritance/CMakeLists.txt | 1 - test/util/my_package/CMakeLists.txt | 1 - .../publish_types_manually/CMakeLists.txt | 2 -- test/util/query_test/CMakeLists.txt | 2 -- test/util/stress_testing_utils/CMakeLists.txt | 1 - 15 files changed, 36 insertions(+), 51 deletions(-) diff --git a/.conan/test_packages/package/my_package/CMakeLists.txt b/.conan/test_packages/package/my_package/CMakeLists.txt index 4ec7b1b7..92dfe5fb 100644 --- a/.conan/test_packages/package/my_package/CMakeLists.txt +++ b/.conan/test_packages/package/my_package/CMakeLists.txt @@ -24,7 +24,6 @@ add_sen_package( target_sources(my_package PRIVATE ${_combined_schema}) target_link_libraries(my_package PRIVATE $) set_target_properties(my_package PROPERTIES FOLDER "examples/basic") -set_target_properties(my_package PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # TODO(SEN-1185): enable tests when possible configure_file(config/my_package.yaml ${CMAKE_BINARY_DIR}/test_configs/my_package.yaml COPYONLY) diff --git a/apps/cli_gen/test/CMakeLists.txt b/apps/cli_gen/test/CMakeLists.txt index 1e10a197..0b0a614f 100644 --- a/apps/cli_gen/test/CMakeLists.txt +++ b/apps/cli_gen/test/CMakeLists.txt @@ -6,7 +6,7 @@ # ====================================================================================================================== set(hla_fom_gen_test_sources ${CMAKE_CURRENT_SOURCE_DIR}) -set(working_dir ${CMAKE_BINARY_DIR}/bin) +set(working_dir $) # Test case 1 # diff --git a/cmake/util/sen_misc_utils.cmake b/cmake/util/sen_misc_utils.cmake index fff0d109..f2180ce1 100644 --- a/cmake/util/sen_misc_utils.cmake +++ b/cmake/util/sen_misc_utils.cmake @@ -639,12 +639,20 @@ function(sen_configure_target target_name) endif() endif() - set_property(TARGET ${target_name} PROPERTY LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin" - )# .exe and .dll - set_property(TARGET ${target_name} PROPERTY RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin" - )# .so and .dylib - set_property(TARGET ${target_name} PROPERTY ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib" - )# .a and .lib + if(NOT DEFINED CMAKE_LIBRARY_OUTPUT_DIRECTORY) + set_property(TARGET ${target_name} PROPERTY LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin" + )# .exe and .dll + endif() + + if(NOT DEFINED CMAKE_RUNTIME_OUTPUT_DIRECTORY) + set_property(TARGET ${target_name} PROPERTY RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin" + )# .so and .dylib + endif() + + if(NOT DEFINED CMAKE_ARCHIVE_OUTPUT_DIRECTORY) + set_property(TARGET ${target_name} PROPERTY ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib" + )# .a and .lib + endif() # link coverage flags if coverage was enabled (interface target created in root CMakeLists.txt) if(TARGET sen_coverage_flags) diff --git a/cmake/util/test.cmake b/cmake/util/test.cmake index 0ee3c982..1af56b62 100644 --- a/cmake/util/test.cmake +++ b/cmake/util/test.cmake @@ -220,20 +220,21 @@ function(add_sen_run_smoke_test test_name) message(FATAL_ERROR "add_sen_run_smoke_test: no CONFIG_FILE set") endif() - if(NOT - CMAKE_GENERATOR - STREQUAL - "Ninja" + if(_arg_WORKING_DIRECTORY) + set(_working_dir ${_arg_WORKING_DIRECTORY}) + elseif(DEFINED CMAKE_RUNTIME_OUTPUT_DIRECTORY) + set(_working_dir ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + elseif( + NOT + CMAKE_GENERATOR + STREQUAL + "Ninja" ) set(_working_dir ${PROJECT_BINARY_DIR}/bin/${CMAKE_BUILD_TYPE}) else() set(_working_dir ${PROJECT_BINARY_DIR}/bin) endif() - if(_arg_WORKING_DIRECTORY) - set(_working_dir ${_arg_WORKING_DIRECTORY}) - endif() - get_filename_component(_abs_config ${_arg_CONFIG_FILE} ABSOLUTE) if(_arg_NO_START_STOP) @@ -343,7 +344,7 @@ function(add_sen_cli_gen_smoke_test test_name) ) endfunction() -# add_sen_cli_gen_smoke_test( +# append_test_env_modification( # # [list of modifications] # ) diff --git a/components/py/test/CMakeLists.txt b/components/py/test/CMakeLists.txt index c440decd..b75919b7 100644 --- a/components/py/test/CMakeLists.txt +++ b/components/py/test/CMakeLists.txt @@ -16,7 +16,7 @@ add_sen_integration_test( COMMAND ./cli_run ${CMAKE_CURRENT_SOURCE_DIR}/config/modify_objects.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_DEPS cli_run ) diff --git a/libs/kernel/test/integration/crash_report/CMakeLists.txt b/libs/kernel/test/integration/crash_report/CMakeLists.txt index f1f0bf3c..56d025cd 100644 --- a/libs/kernel/test/integration/crash_report/CMakeLists.txt +++ b/libs/kernel/test/integration/crash_report/CMakeLists.txt @@ -19,14 +19,13 @@ add_sen_package( ) set_target_properties(crash_report PROPERTIES FOLDER "test") -set_target_properties(crash_report PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_integration_test( kernel_crash_report_stacktrace_test COMMAND ${PYTEST_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/crash_report_tester.py - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_DEPS crash_report ) diff --git a/libs/kernel/test/integration/runtime_compatibility/CMakeLists.txt b/libs/kernel/test/integration/runtime_compatibility/CMakeLists.txt index d587396b..5e057ddf 100644 --- a/libs/kernel/test/integration/runtime_compatibility/CMakeLists.txt +++ b/libs/kernel/test/integration/runtime_compatibility/CMakeLists.txt @@ -24,7 +24,6 @@ add_sen_package( sen_enable_static_analysis(runtime_1) set_target_properties(runtime_1 PROPERTIES FOLDER "test") -set_target_properties(runtime_1 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_package( TARGET runtime_2 @@ -42,7 +41,6 @@ add_sen_package( sen_enable_static_analysis(runtime_2) set_target_properties(runtime_2 PROPERTIES FOLDER "test") -set_target_properties(runtime_2 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # Generate the needed packages add_sen_package( @@ -61,7 +59,6 @@ add_sen_package( sen_enable_static_analysis(runtime_3) set_target_properties(runtime_3 PROPERTIES FOLDER "test") -set_target_properties(runtime_3 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_package( TARGET runtime_4 @@ -79,7 +76,6 @@ add_sen_package( sen_enable_static_analysis(runtime_4) set_target_properties(runtime_4 PROPERTIES FOLDER "test") -set_target_properties(runtime_4 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_package( TARGET runtime_5 @@ -97,7 +93,6 @@ add_sen_package( sen_enable_static_analysis(runtime_5) set_target_properties(runtime_5 PROPERTIES FOLDER "test") -set_target_properties(runtime_5 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_package( TARGET runtime_6 @@ -115,7 +110,6 @@ add_sen_package( sen_enable_static_analysis(runtime_6) set_target_properties(runtime_6 PROPERTIES FOLDER "test") -set_target_properties(runtime_6 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # runtime compatibility tests # @@ -133,7 +127,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/runtime_1/config/runtime_1.yaml ${CMAKE_CURRENT_SOURCE_DIR}/runtime_2/config/runtime_2.yaml ${CMAKE_CURRENT_SOURCE_DIR}/../tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS runtime_1 runtime_2 ) @@ -151,7 +145,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/runtime_3/config/runtime_3.yaml ${CMAKE_CURRENT_SOURCE_DIR}/runtime_4/config/runtime_4.yaml ${CMAKE_CURRENT_SOURCE_DIR}/../tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS runtime_3 runtime_4 ) @@ -169,7 +163,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/runtime_5/config/runtime_5.yaml ${CMAKE_CURRENT_SOURCE_DIR}/runtime_6/config/runtime_6.yaml ${CMAKE_CURRENT_SOURCE_DIR}/../tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS runtime_5 runtime_6 ) diff --git a/libs/kernel/test/integration/transport/CMakeLists.txt b/libs/kernel/test/integration/transport/CMakeLists.txt index f1843c73..67f9e895 100644 --- a/libs/kernel/test/integration/transport/CMakeLists.txt +++ b/libs/kernel/test/integration/transport/CMakeLists.txt @@ -25,7 +25,6 @@ add_sen_package( ) set_target_properties(transport_1 PROPERTIES FOLDER "test") -set_target_properties(transport_1 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_package( TARGET transport_2 @@ -42,7 +41,6 @@ add_sen_package( ) set_target_properties(transport_2 PROPERTIES FOLDER "test") -set_target_properties(transport_2 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # transport tests # @@ -60,7 +58,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/transport_1/config/transport_1.yaml ${CMAKE_CURRENT_SOURCE_DIR}/transport_2/config/transport_2.yaml ${CMAKE_CURRENT_SOURCE_DIR}/../tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS transport_1 transport_2 ) diff --git a/libs/kernel/test/integration/type_clash/CMakeLists.txt b/libs/kernel/test/integration/type_clash/CMakeLists.txt index c7130501..1c90aa82 100644 --- a/libs/kernel/test/integration/type_clash/CMakeLists.txt +++ b/libs/kernel/test/integration/type_clash/CMakeLists.txt @@ -19,9 +19,7 @@ add_sen_package( NO_SCHEMA ) sen_enable_static_analysis(type_clash_participant_1) -set_target_properties( - type_clash_participant_1 PROPERTIES FOLDER "test" LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin -) +set_target_properties(type_clash_participant_1 PROPERTIES FOLDER "test") add_sen_package( TARGET type_clash_participant_2 @@ -35,9 +33,7 @@ add_sen_package( NO_SCHEMA ) sen_enable_static_analysis(type_clash_participant_2) -set_target_properties( - type_clash_participant_2 PROPERTIES FOLDER "test" LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin -) +set_target_properties(type_clash_participant_2 PROPERTIES FOLDER "test") add_sen_package( TARGET type_clash_participant_3 @@ -51,9 +47,7 @@ add_sen_package( NO_SCHEMA ) sen_enable_static_analysis(type_clash_participant_3) -set_target_properties( - type_clash_participant_3 PROPERTIES FOLDER "test" LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin -) +set_target_properties(type_clash_participant_3 PROPERTIES FOLDER "test") add_sen_integration_test( kernel_type_clash_test_1 @@ -63,7 +57,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/participant_1/config/participant_1.yaml ${CMAKE_CURRENT_SOURCE_DIR}/participant_2/config/participant_2.yaml ${CMAKE_CURRENT_SOURCE_DIR}/type_clash_tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS type_clash_participant_1 type_clash_participant_2 ) @@ -85,7 +79,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/participant_1/config/participant_1.yaml ${CMAKE_CURRENT_SOURCE_DIR}/participant_3/config/participant_3.yaml ${CMAKE_CURRENT_SOURCE_DIR}/type_clash_tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS type_clash_participant_1 type_clash_participant_3 ) diff --git a/test/util/chaos_monkeys/CMakeLists.txt b/test/util/chaos_monkeys/CMakeLists.txt index 3c06b5c6..1e1d2292 100644 --- a/test/util/chaos_monkeys/CMakeLists.txt +++ b/test/util/chaos_monkeys/CMakeLists.txt @@ -23,7 +23,6 @@ add_sen_package( ) set_target_properties(creator_monkeys PROPERTIES FOLDER "test/util/chaos_monkeys") -set_target_properties(creator_monkeys PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # TODO(SEN-1165): investigate flaky results for these tests # # execute creator monkeys test with one type of object with a frequency of 10hz during 10 seconds diff --git a/test/util/inheritance/CMakeLists.txt b/test/util/inheritance/CMakeLists.txt index 281b7258..9d3589ac 100644 --- a/test/util/inheritance/CMakeLists.txt +++ b/test/util/inheritance/CMakeLists.txt @@ -24,6 +24,5 @@ add_sen_package( ) set_target_properties(inheritance PROPERTIES FOLDER "examples/basic") -set_target_properties(inheritance PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_run_smoke_test(inheritance_smoke CONFIG_FILE config/inheritance.yaml) diff --git a/test/util/my_package/CMakeLists.txt b/test/util/my_package/CMakeLists.txt index aa860c4f..e05e7987 100644 --- a/test/util/my_package/CMakeLists.txt +++ b/test/util/my_package/CMakeLists.txt @@ -26,7 +26,6 @@ add_sen_package( ) set_target_properties(my_package PROPERTIES FOLDER "examples/basic") -set_target_properties(my_package PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # smoke tests add_sen_run_smoke_test(my_package_abstract_smoke CONFIG_FILE config/my_package_abstract.yaml WILL_FAIL) diff --git a/test/util/publish_types_manually/CMakeLists.txt b/test/util/publish_types_manually/CMakeLists.txt index 951e400a..7f75d730 100644 --- a/test/util/publish_types_manually/CMakeLists.txt +++ b/test/util/publish_types_manually/CMakeLists.txt @@ -17,6 +17,4 @@ add_sen_package( ) sen_enable_static_analysis(publish_types_manually) -set_target_properties(publish_types_manually PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) - add_sen_run_smoke_test(publish_types_manually CONFIG_FILE config/component.yaml) diff --git a/test/util/query_test/CMakeLists.txt b/test/util/query_test/CMakeLists.txt index d033df1d..dcded654 100644 --- a/test/util/query_test/CMakeLists.txt +++ b/test/util/query_test/CMakeLists.txt @@ -18,6 +18,4 @@ add_sen_package( sen_enable_static_analysis(query_test) -set_target_properties(query_test PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) - add_sen_run_smoke_test(query_test CONFIG_FILE config/component.yaml) diff --git a/test/util/stress_testing_utils/CMakeLists.txt b/test/util/stress_testing_utils/CMakeLists.txt index 49ebf443..b6da33ec 100644 --- a/test/util/stress_testing_utils/CMakeLists.txt +++ b/test/util/stress_testing_utils/CMakeLists.txt @@ -17,4 +17,3 @@ add_sen_package( ) target_link_libraries(stress_testing_utils PRIVATE $) -set_target_properties(stress_testing_utils PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) From 3e971eb46a637cba2da6b203efaa6f43ee1cd5ec Mon Sep 17 00:00:00 2001 From: Aridane Sarrionandia Date: Wed, 20 May 2026 11:09:10 +0200 Subject: [PATCH 10/17] feat(core): Adds mapping properties to fom packages and provides a new cmake function to generate interface only packages * Adds mapping files as target property for fom targets * Adds two cmake functions to generate interface only packages resolves SEN-1615 resolves SEN-1614 --- cmake/util/sen_codegen_utils.cmake | 121 +++++++++++++++++++++++++++ cmake/util/sen_misc_utils.cmake | 51 +++++++++++- cmake/util/sen_package_utils.cmake | 127 +++++++++++++++++++++++++++++ 3 files changed, 296 insertions(+), 3 deletions(-) diff --git a/cmake/util/sen_codegen_utils.cmake b/cmake/util/sen_codegen_utils.cmake index efd6ff52..7339a2d4 100644 --- a/cmake/util/sen_codegen_utils.cmake +++ b/cmake/util/sen_codegen_utils.cmake @@ -426,6 +426,12 @@ function(sen_generate_code) _all_mappings ) set(_mapping_opt "--mappings=${_all_mappings}") + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY HLA_MAPPINGS ${_abs_mappings} + ) + endif() target_sources(${_arg_TARGET} PRIVATE ${_xml_files}) @@ -475,6 +481,121 @@ function(sen_generate_code) endfunction() +# Takes a target and adds Sen properties to it with the fom/stl files. +# +# sen_generate_interface_package( +# TARGET +# [BASE_PATH base_path] +# [HLA_OUTPUT_DIR path] +# [STL_FILES [files...]] +# [HLA_FOM_DIRS [dirs...]] +# [HLA_MAPPINGS_FILE [files...]] +function(sen_generate_interface_package) + + set(_one_value_args TARGET BASE_PATH) + set(_multi_value_args STL_FILES HLA_FOM_DIRS HLA_MAPPINGS_FILE) + + cmake_parse_arguments( + _arg + "${_options}" + "${_one_value_args}" + "${_multi_value_args}" + ${ARGN} + ) + + if(NOT _arg_TARGET) + message(FATAL_ERROR " sen_generate_interface_package: no TARGET set") + endif() + + if(NOT TARGET ${_arg_TARGET}) + message( + FATAL_ERROR + " sen_generate_interface_package: specified TARGET has not been created. Define the target before calling this function" + ) + endif() + + if(_arg_STL_FILES AND _arg_HLA_FOM_DIRS) + message( + FATAL_ERROR + " sen_generate_interface_package: STL_FILES and HLA_FOM_DIRS cannot be present at the same time" + ) + endif() + + if(_arg_HLA_MAPPINGS_FILE AND NOT _arg_HLA_FOM_DIRS) + message( + FATAL_ERROR + " sen_generate_interface_package: HLA_MAPPINGS_FILE is defined, but no HLA_FOM_DIRS were specified" + ) + endif() + + # get the absolute base path + if(DEFINED _arg_BASE_PATH AND NOT (_arg_BASE_PATH STREQUAL "")) + get_filename_component(_abs_base_path ${_arg_BASE_PATH} ABSOLUTE) + else() + set(_abs_base_path ${CMAKE_CURRENT_SOURCE_DIR}) + endif() + + set_property(TARGET ${_arg_TARGET} PROPERTY BASE_PATH ${_abs_base_path}) + + # set the include path as extra options for the code generation + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY SEN_IMPORT_DIRS -i ${_abs_base_path} + ) + + if(_arg_STL_FILES) + foreach(_stl_file ${_arg_STL_FILES}) + # get the absolute path to the sen file + get_filename_component(_abs_stl_file ${_stl_file} ABSOLUTE) + get_filename_component(_stl_file_name ${_stl_file} NAME) + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY STL_FILES ${_abs_stl_file} + ) + endforeach() + endif(_arg_STL_FILES) + + # generate from HLA FOM XMLs + if(_arg_HLA_FOM_DIRS) + set(_abs_fom_dirs) + set(_fom_generated_files) + + foreach(_fom_dir ${_arg_HLA_FOM_DIRS}) + get_filename_component(_abs_fom_dir ${_fom_dir} ABSOLUTE) + list(APPEND _abs_fom_dirs ${_abs_fom_dir}) + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY HLA_FOM_DIRS ${_abs_fom_dir} + ) + + # get the xml files + file( + GLOB _xml_files + LIST_DIRECTORIES false + "${_fom_dir}/*.xml" + ) + endforeach() + + if(_arg_HLA_MAPPINGS_FILE) + set(_abs_mappings) + foreach(_mappings_file ${_arg_HLA_MAPPINGS_FILE}) + get_filename_component(_abs_mapping_file ${_mappings_file} ABSOLUTE) + list(APPEND _abs_mappings ${_abs_mapping_file}) + endforeach() + + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY HLA_MAPPINGS ${_abs_mappings} + ) + endif() + endif() + +endfunction() + # Convenience wrapper for sen_generate_code() that generates C++ code (the default). # Accepts the same arguments as sen_generate_code() except LANG, which is fixed to cpp. macro(sen_generate_cpp) diff --git a/cmake/util/sen_misc_utils.cmake b/cmake/util/sen_misc_utils.cmake index f2180ce1..77ec8307 100644 --- a/cmake/util/sen_misc_utils.cmake +++ b/cmake/util/sen_misc_utils.cmake @@ -313,6 +313,7 @@ function(get_external_interfaces) get_target_property(_build_stl_files ${_arg_TARGET} STL_FILES) get_target_property(_build_hla_fom_dirs ${_arg_TARGET} HLA_FOM_DIRS) + get_target_property(_build_hla_mappings ${_arg_TARGET} HLA_MAPPINGS) get_target_property(_build_base_path ${_arg_TARGET} BASE_PATH) string( REGEX MATCH @@ -326,6 +327,12 @@ function(get_external_interfaces) _hla_fom_dirs_present ${_build_hla_fom_dirs} ) + string( + REGEX MATCH + NOTFOUND + _hla_mappings_present + ${_build_hla_mappings} + ) # set base_path that should be common to all interfaces set_property(TARGET ${_arg_TARGET} PROPERTY INSTALL_BASE_PATH ${_abs_install_dir}/interfaces/) @@ -382,7 +389,7 @@ function(get_external_interfaces) if(NOT EXISTS ${installation_fom_dir}) message( FATAL_ERROR - "get_external_interfaces: Could not obtain external interfaces for TARGET ${_arg_TARGET}: Directory ${installation_stl} could not be found on system.\ + "get_external_interfaces: Could not obtain external interfaces for TARGET ${_arg_TARGET}: Directory ${installation_fom_dir} could not be found on system.\ Please, contact with the maintainers of the target. \n\ Possible reasons of the failure: \n\ \tWrongly inputted BASE_PATH on original package generation.\n\ @@ -390,8 +397,8 @@ function(get_external_interfaces) \tINSTALL directory not compliant with the BASE_PATH. \n\ Report this information to the developer when suggesting a fix: \n\ - \tFile was originally built in ${stl_file}\n\ - \tFile was expected to be in ${_abs_install_dir}/${relative_stl_path}\ + \tFOM folder ${fom_dir}\n\ + \tFolder was expected to be in ${_abs_install_dir}/${relative_fom_dir_path}\ Please check the interfaces consumption guide on Sen's official documentation for further information. " @@ -405,6 +412,43 @@ function(get_external_interfaces) endif() endforeach() endif() + # set hla mappings if found + if(_build_hla_mappings) + foreach(mappings_file ${_build_hla_mappings}) + string( + REPLACE ${_build_base_path} + "" + relative_mappings_file_path + ${mappings_file} + ) + set(installation_mappings_file "${_abs_install_dir}/${relative_mappings_file_path}") + + if(NOT EXISTS ${installation_mappings_file}) + message( + FATAL_ERROR + "get_external_interfaces: Could not obtain external interfaces for TARGET ${_arg_TARGET}: Directory ${installation_mappings_file} could not be found on system.\ + Please, contact with the maintainers of the target. \n\ + Possible reasons of the failure: \n\ + \tWrongly inputted BASE_PATH on original package generation.\n\ + \tNo INSTALL directive on the FOM directories.\n\ + \tINSTALL directory not compliant with the BASE_PATH. \n\ + + Report this information to the developer when suggesting a fix: \n\ + \tMappings file was originally built in ${mappings_file}\n\ + \tFile was expected to be in ${_abs_install_dir}/${relative_mappings_file_path}\ + + Please check the interfaces consumption guide on Sen's official documentation for further information. + " + ) + else() + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY INSTALL_HLA_MAPPINGS ${installation_mappings_file} + ) + endif() + endforeach() + endif() endfunction() # Parse all the -config.cmake.in files in the current directory and install the generated -config.cmake files into the target @@ -480,6 +524,7 @@ function(copy_target_properties to_target from_target) copy_target_property(${to_target} ${from_target} SEN_IMPORT_DIRS) copy_target_property(${to_target} ${from_target} STL_FILES) copy_target_property(${to_target} ${from_target} HLA_FOM_DIRS) + copy_target_property(${to_target} ${from_target} HLA_MAPPINGS) copy_target_property(${to_target} ${from_target} EXPORT_FILES) copy_target_property(${to_target} ${from_target} SEN_EXPORTS_TYPES) copy_target_property(${to_target} ${from_target} SEN_IS_PYTHON) diff --git a/cmake/util/sen_package_utils.cmake b/cmake/util/sen_package_utils.cmake index 9bd3b7a4..60e5aae5 100644 --- a/cmake/util/sen_package_utils.cmake +++ b/cmake/util/sen_package_utils.cmake @@ -344,6 +344,133 @@ function(add_sen_package) endfunction() +# Creates a Sen package: an INTERFACE library that contains STL or HLA FOM files. +# +# add_sen_interface_package( +# TARGET +# Name of the CMake target to create. +# +# [MAINTAINER ] +# Person or team responsible for this package. Defaults to "unknown". +# Note: AUTHOR is a deprecated alias for MAINTAINER. +# +# [DESCRIPTION ] +# Human-readable description embedded in the package metadata. +# +# [VERSION ] +# Package version string. Defaults to the CMake project version. +# +# [BASE_PATH ] +# Root directory used to compute relative paths for generated files and +# interface resolution. Defaults to CMAKE_CURRENT_SOURCE_DIR. +# +# [DEPS ] +# Public dependencies. Linked with PUBLIC linkage; their exported Sen types +# are re-exported by this package. +# +# [STL_FILES ] +# Sen Type Language (.stl) files from which C++ code is generated. +# Mutually exclusive with HLA_FOM_DIRS. +# +# [HLA_FOM_DIRS ] +# Directories containing HLA FOM XML files from which C++ code is generated. +# Mutually exclusive with STL_FILES. +# +# [HLA_MAPPINGS_FILE ] +# HLA mapping files that customise FOM-to-C++ translation. +# Requires HLA_FOM_DIRS. +# +function(add_sen_interface_package) + + set(_one_value_args + TARGET + MAINTAINER + DESCRIPTION + VERSION + BASE_PATH + ) + + set(_multi_value_args + DEPS + STL_FILES + HLA_FOM_DIRS + HLA_MAPPINGS_FILE + ) + + cmake_parse_arguments( + _arg + "${_options}" + "${_one_value_args}" + "${_multi_value_args}" + ${ARGN} + ) + + if(NOT _arg_TARGET) + message(FATAL_ERROR "add_sen_interface_package: no TARGET set") + endif() + + if(NOT _arg_MAINTAINER AND NOT _arg_AUTHOR) + set(_arg_MAINTAINER "unknown") + endif() + + if(NOT _arg_VERSION) + set(_arg_VERSION ${CMAKE_PROJECT_VERSION}) + endif() + + if(_arg_BASE_PATH) + get_filename_component(_abs_base_path ${_arg_BASE_PATH} ABSOLUTE) + else() + set(_abs_base_path ${CMAKE_CURRENT_SOURCE_DIR}) + endif() + + add_library(${_arg_TARGET} INTERFACE) + + get_git_head_revision(git_ref_spec git_hash ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR) + git_local_changes(git_status_str) + target_compile_definitions( + ${_arg_TARGET} + INTERFACE SEN_TARGET_NAME="${_arg_TARGET}" + SEN_TARGET_MAINTAINER="${_arg_MAINTAINER}" + SEN_TARGET_DESCRIPTION="${_arg_DESCRIPTION}" + SEN_TARGET_VERSION="${_arg_VERSION}" + GIT_REF_SPEC="${git_ref_spec}" + GIT_HASH="${git_hash}" + GIT_STATUS="${git_status_str}" + ) + + # Copy dependency import paths + if(_arg_DEPS) + foreach(_item ${_arg_DEPS}) + copy_target_property(${_arg_TARGET} ${_item} SEN_IMPORT_DIRS) + endforeach() + endif() + + # generate code if needed + if(_arg_STL_FILES OR _arg_HLA_FOM_DIRS) + if(_arg_STL_FILES) + sen_generate_interface_package( + TARGET + ${_arg_TARGET} + BASE_PATH + ${_arg_BASE_PATH} + STL_FILES + ${_arg_STL_FILES} + ) + else() + sen_generate_interface_package( + TARGET + ${_arg_TARGET} + BASE_PATH + ${_arg_BASE_PATH} + HLA_FOM_DIRS + ${_arg_HLA_FOM_DIRS} + HLA_MAPPINGS_FILE + ${_arg_HLA_MAPPINGS_FILE} + ) + endif() + endif() +endfunction() + # Deprecated wrapper for add_sen_package(... PUBLIC_SYMBOLS). # Kept for backwards compatibility — prefer add_sen_package() with PUBLIC_SYMBOLS directly. macro(sen_generate_package) From 8da70af12795404c16901e650d3d5eaf97549fbd Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Wed, 20 May 2026 12:54:51 +0200 Subject: [PATCH 11/17] fix(core)!: generate MaybeXX types for optional parameters in FOM generation BREAKING CHANGE: Generated C++ structs for optional parameters will now use MaybeXX instead of the base type. Code relying on direct field access will fail to compile. Users must update their code to handle these fields by checking their existence before accessing the underlying value; and by constructing the MaybeXX type explicitly when assigning values. resolves SEN-1644 --- apps/cli_gen/test/CMakeLists.txt | 17 +++++ apps/cli_gen/test/test20/fom/moduleA-20.xml | 49 +++++++++++++ apps/cli_gen/test/test20/test.py | 56 ++++++++++++++ examples/config/9_hla_servers/readme.md | 2 +- .../packages/weather_server/src/randomize.h | 73 +++++++++++++------ libs/core/src/lang/fom_document_set.cpp | 17 ++++- 6 files changed, 186 insertions(+), 28 deletions(-) create mode 100644 apps/cli_gen/test/test20/fom/moduleA-20.xml create mode 100755 apps/cli_gen/test/test20/test.py diff --git a/apps/cli_gen/test/CMakeLists.txt b/apps/cli_gen/test/CMakeLists.txt index 0b0a614f..25a54e20 100644 --- a/apps/cli_gen/test/CMakeLists.txt +++ b/apps/cli_gen/test/CMakeLists.txt @@ -272,3 +272,20 @@ add_sen_integration_test( # link them so the checker only runs if the generator succeeds set_tests_properties(hla_fom_gen_integration_test_19 PROPERTIES ENVIRONMENT "PYTHONUNBUFFERED=1") + +# Test case 20 +# +# ModuleA-20 defines interactions with optional and required parameters +# +# Tests that the cli_gen generates MaybeXX types for optional parameters and standard types for required parameters +add_sen_integration_test( + hla_fom_gen_integration_test_20 + COMMAND + ${Python3_EXECUTABLE} + ${hla_fom_gen_test_sources}/test20/test.py + WORKING_DIRECTORY ${working_dir} + REQ_DEPS cli_gen +) + +# link them so the checker only runs if the generator succeeds +set_tests_properties(hla_fom_gen_integration_test_20 PROPERTIES ENVIRONMENT "PYTHONUNBUFFERED=1") diff --git a/apps/cli_gen/test/test20/fom/moduleA-20.xml b/apps/cli_gen/test/test20/fom/moduleA-20.xml new file mode 100644 index 00000000..f7512f77 --- /dev/null +++ b/apps/cli_gen/test/test20/fom/moduleA-20.xml @@ -0,0 +1,49 @@ + + + + New Module + FOM + 1.0 + unclassified + + + Description of New Module + + + + + + HLAobjectRoot + + + + + HLAinteractionRoot + + MyInteraction + PublishSubscribe + HLAreliable + Receive + Test interaction + + MyOptionalParam + IntModuleA + Optional. This is optional. + + + MyRequiredParam + IntModuleA + This is required. + + + + + + + + IntModuleA + HLAinteger32BE + + + + diff --git a/apps/cli_gen/test/test20/test.py b/apps/cli_gen/test/test20/test.py new file mode 100755 index 00000000..8ab3939d --- /dev/null +++ b/apps/cli_gen/test/test20/test.py @@ -0,0 +1,56 @@ +# === test.py ========================================================================================================== +# Sen Infrastructure +# Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +# See the LICENSE.txt file for more information. +# © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +# ====================================================================================================================== + +import subprocess +import sys +import os + +# integration test to check that the FOM generator creates Maybe types for optional parameters +if __name__ == "__main__": + + # input arguments + exe_path = "./cli_gen" + source_dir = os.path.dirname(os.path.abspath(__file__)) + output_file = os.path.join(os.getcwd(), "fom", "modulea-20.xml.h") + + target_optional = "MaybeI32 myOptionalParam" + target_required = "IntModuleA myRequiredParam" + + cmd = [ + exe_path, + "cpp", + "fom", + f"--directories={source_dir}/fom" + ] + + print(f"Executing: {' '.join(cmd)}") + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode != 0: + print(f"Error: cli_gen failed with exit code {result.returncode}") + print(f"Stdout: {result.stdout}") + print(f"Stderr: {result.stderr}") + sys.exit(1) + + if not os.path.exists(output_file): + print(f"Error: Generated file NOT FOUND at {output_file}") + sys.exit(1) + + with open(output_file, 'r') as f: + content = f.read() + + if target_optional not in content: + print(f"Failure: Optional parameter generation failed. '{target_optional}' not found in the file {output_file}.") + sys.exit(1) + + if target_required not in content: + print(f"Failure: Required parameter generation failed. '{target_required}' not found in the file {output_file}.") + sys.exit(1) + + print(f"Success: Found correctly generated parameters in {output_file}") + sys.exit(0) diff --git a/examples/config/9_hla_servers/readme.md b/examples/config/9_hla_servers/readme.md index f41f862a..30c5c9bc 100644 --- a/examples/config/9_hla_servers/readme.md +++ b/examples/config/9_hla_servers/readme.md @@ -83,5 +83,5 @@ In this case you need to populate the parameters in a more involved manner due t data model: ``` -my.tutorial.weatherServer.reqWeather {"type": "GeodeticLocation", "value": { "lat": 0, "lon": 0}}, false +my.tutorial.weatherServer.reqWeather {"type": "GeodeticLocation", "value": { "latitude": 0, "longitude": 0}}, false ``` diff --git a/examples/packages/weather_server/src/randomize.h b/examples/packages/weather_server/src/randomize.h index d2044864..beeca90c 100644 --- a/examples/packages/weather_server/src/randomize.h +++ b/examples/packages/weather_server/src/randomize.h @@ -9,6 +9,7 @@ #define SEN_WEATHER_SERVER_RANDOMIZE_H // generated code +#include "hla_fom/hla.stl.h" #include "netn/netn-metoc.xml.h" // sen @@ -32,17 +33,26 @@ template void randomizeData(T& result, const netn::GeoReferenceVariant& ref) { result.geoReference = ref; - result.barometricPressure = getRand(0.0f, 50.0f); - result.humidity = getRand(0.0f, 100.0f); + result.barometricPressure = hla::MaybeF32(getRand(0.0f, 50.0f)); + result.humidity = hla::MaybeF32(getRand(0.0f, 100.0f)); result.temperature = getRand(-40.0f, 40.0f); result.visibilityRange = getRand(0.0f, 50000.0f); - result.haze.density = getRand(0.0f, 1.0f); - result.haze.type = netn::HazeTypeEnum32::fog; - result.precipitation.intensity = getRand(0.0f, 1.0f); - result.precipitation.type = netn::PrecipitationTypeEnum32::rain; - result.wind.direction = getRand(0.0f, 360.0f); - result.wind.horizontalSpeed = getRand(0.0f, 500.0f); - result.wind.verticalSpeed = getRand(0.0f, 500.0f); + + netn::HazeStruct haze; + haze.density = getRand(0.0f, 1.0f); + haze.type = netn::HazeTypeEnum32::fog; + result.haze = haze; + + netn::PrecipitationStruct precipitation; + precipitation.intensity = getRand(0.0f, 1.0f); + precipitation.type = netn::PrecipitationTypeEnum32::rain; + result.precipitation = precipitation; + + netn::WindStruct wind; + wind.direction = getRand(0.0f, 360.0f); + wind.horizontalSpeed = getRand(0.0f, 500.0f); + wind.verticalSpeed = getRand(0.0f, 500.0f); + result.wind = wind; } template @@ -68,9 +78,13 @@ struct MakeRandom { netn::TroposphereLayerCondition elem {}; randomizeData(elem, ref); - elem.cloud.coverage = getRand(0.0f, 1.0f); - elem.cloud.density = getRand(0.0f, 1.0f); - elem.cloud.type = netn::CloudTypeEnum32::cirrostratus; + + netn::CloudStruct cloud; + cloud.coverage = getRand(0.0f, 1.0f); + cloud.density = getRand(0.0f, 1.0f); + cloud.type = netn::CloudTypeEnum32::cirrostratus; + elem.cloud = cloud; + return elem; } }; @@ -82,12 +96,19 @@ struct MakeRandom { netn::WaterSurfaceCondition elem {}; randomizeData(elem, ref); - elem.ice.coverage = getRand(0.0f, 100.0f); - elem.ice.thickness = getRand(0.0f, 3000.0f); - elem.ice.type = netn::IceTypeEnum16::ice; - elem.current.direction = getRand(0.0f, 360.0f); - elem.current.speed = getRand(0.0f, 50.0f); - elem.salinity = getRand(0.0f, 20.0f); + + netn::IceStruct ice; + ice.coverage = getRand(0.0f, 100.0f); + ice.thickness = getRand(0.0f, 3000.0f); + ice.type = netn::IceTypeEnum16::ice; + elem.ice = ice; + + netn::CurrentStruct current; + current.direction = getRand(0.0f, 360.0f); + current.speed = getRand(0.0f, 50.0f); + elem.current = current; + + elem.salinity = hla::MaybeF32(getRand(0.0f, 20.0f)); return elem; } }; @@ -99,11 +120,15 @@ struct MakeRandom { netn::SubsurfaceLayerCondition elem {}; elem.geoReference = ref; - elem.current.direction = getRand(0.0f, 360.0f); - elem.current.speed = getRand(0.0f, 50.0f); - elem.salinity = getRand(0.0f, 20.0f); + + netn::CurrentStruct current; + current.direction = getRand(0.0f, 360.0f); + current.speed = getRand(0.0f, 50.0f); + elem.current = current; + + elem.salinity = hla::MaybeF32(getRand(0.0f, 20.0f)); elem.temperature = getRand(-20.0f, 400.0f); - elem.bottomType = netn::SedimentTypeEnum32::mud; + elem.bottomType = netn::MaybeSedimentTypeEnum32(netn::SedimentTypeEnum32::mud); return elem; } }; @@ -115,8 +140,8 @@ struct MakeRandom { netn::LandSurfaceCondition elem {}; randomizeData(elem, ref); - elem.moisture = netn::SurfaceMoistureEnum16::wet; - elem.iceCondition = netn::RoadIceConditionEnum16::patches; + elem.moisture = netn::MaybeSurfaceMoistureEnum16(netn::SurfaceMoistureEnum16::wet); + elem.iceCondition = netn::MaybeRoadIceConditionEnum16(netn::RoadIceConditionEnum16::patches); return elem; } }; diff --git a/libs/core/src/lang/fom_document_set.cpp b/libs/core/src/lang/fom_document_set.cpp index 1d4759cb..b4daeacc 100644 --- a/libs/core/src/lang/fom_document_set.cpp +++ b/libs/core/src/lang/fom_document_set.cpp @@ -1670,9 +1670,20 @@ std::vector FomDocumentSet::collectArgsFromInteractionNode(const pugi::xpat continue; } - nodeArgs.emplace_back(toLowerCamelCase(paramName), - formatSemantics(param.node().child_value("semantics")), - getOrCreateTypeFromFomName(param.node().child_value("dataType"), doc).first); + std::string semanticsString = param.node().child_value("semantics"); + + auto argType = [&]() + { + if (startsWith(semanticsString, "Optional.") || startsWith(semanticsString, "Optional (") || + startsWith(semanticsString, "Optional:") || startsWith(semanticsString, "Optional,")) + { + return getOptionalPropertyType(param.node().child_value("dataType"), doc); + } + + return getOrCreateTypeFromFomName(param.node().child_value("dataType"), doc).first; + }(); + + nodeArgs.emplace_back(toLowerCamelCase(paramName), formatSemantics(semanticsString), argType); } result = prependArgs(result, nodeArgs); From 478a0c8fb7ae022188fa5222b4bd35971fa882b9 Mon Sep 17 00:00:00 2001 From: Florian Sattler Date: Wed, 20 May 2026 18:21:19 +0200 Subject: [PATCH 12/17] fix: compile errors with local linux dependencies * compile error on linux systems missing default environ * remove unnecessary system dependenct linux headers resolves SEN-1662 --- libs/kernel/src/crash_reporter.cpp | 4 ++-- libs/kernel/src/posix/posix_api.cpp | 5 +---- libs/kernel/src/posix/thread_impl.cpp | 5 ----- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/libs/kernel/src/crash_reporter.cpp b/libs/kernel/src/crash_reporter.cpp index 04f4b094..4b0852a7 100644 --- a/libs/kernel/src/crash_reporter.cpp +++ b/libs/kernel/src/crash_reporter.cpp @@ -72,8 +72,8 @@ #include // with Apple we need to explicitly declare this as an external symbol -#ifdef __APPLE__ -extern "C" char** environ; +#if defined(__APPLE__) || defined(__linux__) +# include #endif namespace sen::kernel::impl diff --git a/libs/kernel/src/posix/posix_api.cpp b/libs/kernel/src/posix/posix_api.cpp index 331b2e2a..6c26fee0 100644 --- a/libs/kernel/src/posix/posix_api.cpp +++ b/libs/kernel/src/posix/posix_api.cpp @@ -8,10 +8,7 @@ #include "./posix_api.h" // system -#ifdef __linux__ -# include -# include -#elif defined(__APPLE__) +#if defined(__APPLE__) # include "mach/mach.h" # include "mach/thread_act.h" # include "mach/thread_policy.h" diff --git a/libs/kernel/src/posix/thread_impl.cpp b/libs/kernel/src/posix/thread_impl.cpp index ef065383..562e55f0 100644 --- a/libs/kernel/src/posix/thread_impl.cpp +++ b/libs/kernel/src/posix/thread_impl.cpp @@ -16,11 +16,6 @@ // generated code #include "stl/sen/kernel/basic_types.stl.h" -// system -#ifdef __linux__ -# include -#endif - // other posix #include From 013ed8c04dd8bb3fcf49476f22e59e8ae4769752 Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Thu, 21 May 2026 11:26:45 +0200 Subject: [PATCH 13/17] feat: provide public access of toJson serialization resolves SEN-1628 --- apps/cli_gen/src/cpp/templates/struct_impl.j2 | 13 + .../cli_gen/src/cpp/templates/variant_impl.j2 | 13 + .../core/include/sen/core/meta/basic_traits.h | 21 +- .../sen/core/meta/detail/native_types_impl.h | 2 + libs/core/include/sen/core/meta/enum_traits.h | 22 ++ .../include/sen/core/meta/optional_traits.h | 16 ++ .../include/sen/core/meta/quantity_traits.h | 22 ++ .../include/sen/core/meta/sequence_traits.h | 33 +++ libs/core/include/sen/core/meta/time_types.h | 34 +++ libs/core/include/sen/core/meta/type_traits.h | 2 + libs/core/include/sen/core/obj/detail/gen.h | 14 + libs/core/test/CMakeLists.txt | 1 + .../test/io/serialization_traits_test.cpp | 257 ++++++++++++++++++ 13 files changed, 448 insertions(+), 2 deletions(-) diff --git a/apps/cli_gen/src/cpp/templates/struct_impl.j2 b/apps/cli_gen/src/cpp/templates/struct_impl.j2 index ae77007a..c6a83491 100644 --- a/apps/cli_gen/src/cpp/templates/struct_impl.j2 +++ b/apps/cli_gen/src/cpp/templates/struct_impl.j2 @@ -204,3 +204,16 @@ sen::ConstTypeHandle<::sen::StructType> sen::MetaTypeTrait<{{ qualType }}>::meta }); return type; } + +std::string sen::SerializationTraits<{{ qualType }}>::toJsonString(const {{ qualType }}& val) +{ + ::sen::Var var; + ::sen::VariantTraits<{{ qualType }}>::valueToVariant(val, var); + return ::sen::toJson(var); +} + +void sen::SerializationTraits<{{ qualType }}>::fromJsonString(const std::string& str, {{ qualType }}& val) +{ + const ::sen::Var var = ::sen::fromJson(str); + ::sen::VariantTraits<{{ qualType }}>::variantToValue(var, val); +} diff --git a/apps/cli_gen/src/cpp/templates/variant_impl.j2 b/apps/cli_gen/src/cpp/templates/variant_impl.j2 index a7eb32c8..876e51f6 100644 --- a/apps/cli_gen/src/cpp/templates/variant_impl.j2 +++ b/apps/cli_gen/src/cpp/templates/variant_impl.j2 @@ -173,3 +173,16 @@ std::function<::sen::lang::Value(const void*)> sen::VariantTraits<{{ qualType }} }); return type; } + +std::string sen::SerializationTraits<{{ qualType }}>::toJsonString(const {{ qualType }}& val) +{ + ::sen::Var var; + ::sen::VariantTraits<{{ qualType }}>::valueToVariant(val, var); + return ::sen::toJson(var); +} + +void sen::SerializationTraits<{{ qualType }}>::fromJsonString(const std::string& str, {{ qualType }}& val) +{ + const ::sen::Var var = ::sen::fromJson(str); + ::sen::VariantTraits<{{ qualType }}>::variantToValue(var, val); +} diff --git a/libs/core/include/sen/core/meta/basic_traits.h b/libs/core/include/sen/core/meta/basic_traits.h index ee4005d5..9f847e58 100644 --- a/libs/core/include/sen/core/meta/basic_traits.h +++ b/libs/core/include/sen/core/meta/basic_traits.h @@ -8,11 +8,15 @@ #ifndef SEN_CORE_META_BASIC_TRAITS_H #define SEN_CORE_META_BASIC_TRAITS_H -#include "sen/core/base/duration.h" -#include "sen/core/base/timestamp.h" +// sen #include "sen/core/io/detail/serialization_traits.h" #include "sen/core/io/util.h" #include "sen/core/meta/type.h" +#include "sen/core/meta/var.h" + +// std +#include +#include namespace sen { @@ -38,6 +42,19 @@ struct BasicTraits static void variantToValue(const Var& var, T& val) { val = getCopyAs(var); } static void valueToVariant(T val, Var& var) { var = val; } static constexpr uint32_t serializedSize(T val) noexcept { return impl::getSerializedSize(val); } + + static std::string toJsonString(const T& val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// @} diff --git a/libs/core/include/sen/core/meta/detail/native_types_impl.h b/libs/core/include/sen/core/meta/detail/native_types_impl.h index 0c6bc6ba..381a654d 100644 --- a/libs/core/include/sen/core/meta/detail/native_types_impl.h +++ b/libs/core/include/sen/core/meta/detail/native_types_impl.h @@ -108,6 +108,8 @@ class RealTypeBase: public Parent static inline void write(OutputStream& out, native val) { out.writefunc(val); } \ static inline void read(InputStream& in, native& val) { in.readfunc(val); } \ using BasicTraits::serializedSize; \ + using BasicTraits::toJsonString; \ + using BasicTraits::fromJsonString; \ } //-------------------------------------------------------------------------------------------------------------- diff --git a/libs/core/include/sen/core/meta/enum_traits.h b/libs/core/include/sen/core/meta/enum_traits.h index 4dccd304..eb4b94ac 100644 --- a/libs/core/include/sen/core/meta/enum_traits.h +++ b/libs/core/include/sen/core/meta/enum_traits.h @@ -8,7 +8,16 @@ #ifndef SEN_CORE_META_ENUM_TRAITS_H #define SEN_CORE_META_ENUM_TRAITS_H +// sen +#include "sen/core/io/input_stream.h" +#include "sen/core/io/output_stream.h" #include "sen/core/meta/enum_type.h" +#include "sen/core/meta/var.h" + +// std +#include +#include +#include namespace sen { @@ -30,6 +39,19 @@ struct EnumTraitsBase static void valueToVariant(const T& val, Var& var) { var = static_cast(val); } static void variantToValue(const Var& var, T& val) { impl::enumVariantToValue(var, val); } [[nodiscard]] static uint32_t serializedSize(T val) noexcept { return impl::enumSerializedSize(val); } + + static std::string toJsonString(T val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// @} diff --git a/libs/core/include/sen/core/meta/optional_traits.h b/libs/core/include/sen/core/meta/optional_traits.h index f0d5248e..7c786207 100644 --- a/libs/core/include/sen/core/meta/optional_traits.h +++ b/libs/core/include/sen/core/meta/optional_traits.h @@ -14,9 +14,12 @@ #include "sen/core/io/output_stream.h" #include "sen/core/io/util.h" #include "sen/core/meta/optional_type.h" +#include "sen/core/meta/var.h" // std +#include #include +#include namespace sen { @@ -37,6 +40,19 @@ struct OptionalTraitsBase static void valueToVariant(const T& val, Var& var); static void variantToValue(const Var& var, T& val); [[nodiscard]] static uint32_t serializedSize(T val) noexcept; + + static std::string toJsonString(const T& val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// @} diff --git a/libs/core/include/sen/core/meta/quantity_traits.h b/libs/core/include/sen/core/meta/quantity_traits.h index c111e9f5..4bd6db6b 100644 --- a/libs/core/include/sen/core/meta/quantity_traits.h +++ b/libs/core/include/sen/core/meta/quantity_traits.h @@ -8,8 +8,17 @@ #ifndef SEN_CORE_META_QUANTITY_TRAITS_H #define SEN_CORE_META_QUANTITY_TRAITS_H +// sen +#include "sen/core/io/input_stream.h" +#include "sen/core/io/output_stream.h" #include "sen/core/io/util.h" #include "sen/core/meta/quantity_type.h" +#include "sen/core/meta/type_traits.h" +#include "sen/core/meta/var.h" + +// std +#include +#include namespace sen { @@ -35,6 +44,19 @@ struct QuantityTraitsBase { return SerializationTraits::serializedSize(val.get()); } + + static std::string toJsonString(T val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// @} diff --git a/libs/core/include/sen/core/meta/sequence_traits.h b/libs/core/include/sen/core/meta/sequence_traits.h index 00837074..1bf9bfd8 100644 --- a/libs/core/include/sen/core/meta/sequence_traits.h +++ b/libs/core/include/sen/core/meta/sequence_traits.h @@ -9,8 +9,15 @@ #define SEN_CORE_META_SEQUENCE_TRAITS_H // sen +#include "sen/core/io/input_stream.h" +#include "sen/core/io/output_stream.h" #include "sen/core/io/util.h" #include "sen/core/meta/sequence_type.h" +#include "sen/core/meta/var.h" + +// std +#include +#include namespace sen { @@ -31,6 +38,19 @@ struct SequenceTraitsBase static void valueToVariant(const T& val, Var& var) { impl::sequenceToVariant(val, var); } static void variantToValue(const Var& var, T& val) { impl::variantToSequence(var, val); } [[nodiscard]] static uint32_t serializedSize(const T& val) noexcept { return impl::sequenceSerializedSize(val); } + + static std::string toJsonString(const T& val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// Base class for sequence traits. @@ -44,6 +64,19 @@ struct ArrayTraitsBase static void valueToVariant(const T& val, Var& var) { impl::arrayToVariant(val, var); } static void variantToValue(const Var& var, T& val) { impl::variantToArray(var, val); } [[nodiscard]] static uint32_t serializedSize(const T& val) noexcept { return impl::arraySerializedSize(val); } + + static std::string toJsonString(const T& val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// @} diff --git a/libs/core/include/sen/core/meta/time_types.h b/libs/core/include/sen/core/meta/time_types.h index 6a9c913c..3c30c896 100644 --- a/libs/core/include/sen/core/meta/time_types.h +++ b/libs/core/include/sen/core/meta/time_types.h @@ -11,11 +11,19 @@ // sen #include "sen/core/base/duration.h" #include "sen/core/base/timestamp.h" +#include "sen/core/io/detail/serialization_traits.h" +#include "sen/core/io/input_stream.h" #include "sen/core/meta/basic_traits.h" #include "sen/core/meta/quantity_type.h" #include "sen/core/meta/type.h" +#include "sen/core/meta/type_traits.h" #include "sen/core/meta/unit.h" #include "sen/core/meta/unit_registry.h" +#include "sen/core/meta/var.h" + +// std +#include +#include namespace sen { @@ -131,6 +139,19 @@ struct SerializationTraits { return impl::getSerializedSize(val.get()); } + + static std::string toJsonString(const Duration val) + { + Var var; + VariantTraits::valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, Duration& val) + { + const Var var = fromJson(str); + VariantTraits::variantToValue(var, val); + } }; std::ostream& operator<<(std::ostream& out, const Duration& val); @@ -165,6 +186,19 @@ struct SerializationTraits { return impl::getSerializedSize(val); } + + static std::string toJsonString(const TimeStamp val) + { + Var var; + VariantTraits::valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, TimeStamp& val) + { + const Var var = fromJson(str); + VariantTraits::variantToValue(var, val); + } }; template <> diff --git a/libs/core/include/sen/core/meta/type_traits.h b/libs/core/include/sen/core/meta/type_traits.h index 15698a31..d022db6c 100644 --- a/libs/core/include/sen/core/meta/type_traits.h +++ b/libs/core/include/sen/core/meta/type_traits.h @@ -49,6 +49,8 @@ struct SerializationTraits // static void write(OutputStream& out, const T& val); // static void read(InputStream& in, T& val); // static uint32_t serializedSize(const T& val) noexcept; + // static std::string toJsonString(const T& val); + // static void fromJsonString(const std::string& str, T& val); static_assert(true, "SerializationTraits is not defined for type T."); }; diff --git a/libs/core/include/sen/core/obj/detail/gen.h b/libs/core/include/sen/core/obj/detail/gen.h index fab1ff59..20bcb334 100644 --- a/libs/core/include/sen/core/obj/detail/gen.h +++ b/libs/core/include/sen/core/obj/detail/gen.h @@ -293,6 +293,8 @@ protected: using OptionalTraitsBase::write; \ using OptionalTraitsBase::read; \ using OptionalTraitsBase::serializedSize; \ + using OptionalTraitsBase::toJsonString; \ + using OptionalTraitsBase::fromJsonString; \ }; /// Used by the code generator NOLINTNEXTLINE @@ -314,6 +316,8 @@ protected: using EnumTraitsBase::write; \ using EnumTraitsBase::read; \ using EnumTraitsBase::serializedSize; \ + using EnumTraitsBase::toJsonString; \ + using EnumTraitsBase::fromJsonString; \ }; \ template <> \ struct SEN_MAYBE_EXPORT(doExport) StringConversionTraits \ @@ -343,6 +347,8 @@ protected: using SequenceTraitsBase::write; \ using SequenceTraitsBase::read; \ using SequenceTraitsBase::serializedSize; \ + using SequenceTraitsBase::toJsonString; \ + using SequenceTraitsBase::fromJsonString; \ }; /// Used by the code generator NOLINTNEXTLINE @@ -364,6 +370,8 @@ protected: using ArrayTraitsBase::write; \ using ArrayTraitsBase::read; \ using ArrayTraitsBase::serializedSize; \ + using ArrayTraitsBase::toJsonString; \ + using ArrayTraitsBase::fromJsonString; \ }; /// Used by the code generator NOLINTNEXTLINE @@ -386,6 +394,8 @@ protected: static void write(OutputStream& out, const classname& val); \ static void read(InputStream& in, classname& val); \ [[nodiscard]] static uint32_t serializedSize(const classname& val) noexcept; \ + static std::string toJsonString(const classname& val); \ + static void fromJsonString(const std::string& str, classname& val); \ }; /// Used by the code generator NOLINTNEXTLINE @@ -409,6 +419,8 @@ protected: static void write(OutputStream& out, const classname& val); \ static void read(InputStream& in, classname& val); \ [[nodiscard]] static uint32_t serializedSize(const classname& val) noexcept; \ + static std::string toJsonString(const classname& val); \ + static void fromJsonString(const std::string& str, classname& val); \ }; /// Used by the code generator NOLINTNEXTLINE @@ -520,6 +532,8 @@ protected: using QuantityTraitsBase::write; \ using QuantityTraitsBase::read; \ using QuantityTraitsBase::serializedSize; \ + using QuantityTraitsBase::toJsonString; \ + using QuantityTraitsBase::fromJsonString; \ }; \ template <> \ struct SEN_MAYBE_EXPORT(doExport) QuantityTraits \ diff --git a/libs/core/test/CMakeLists.txt b/libs/core/test/CMakeLists.txt index 5730b2fa..93a3f909 100644 --- a/libs/core/test/CMakeLists.txt +++ b/libs/core/test/CMakeLists.txt @@ -128,6 +128,7 @@ add_sen_unit_test_suite( sen::util PRIVATE cpptrace::cpptrace + nlohmann_json::nlohmann_json core_test_io_generated core_test_meta_generated core_test_obj_generated diff --git a/libs/core/test/io/serialization_traits_test.cpp b/libs/core/test/io/serialization_traits_test.cpp index 278d7bd6..9d24a5df 100644 --- a/libs/core/test/io/serialization_traits_test.cpp +++ b/libs/core/test/io/serialization_traits_test.cpp @@ -6,19 +6,39 @@ // ===================================================================================================================== // sen +#include "sen/core/base/duration.h" #include "sen/core/base/numbers.h" #include "sen/core/base/timestamp.h" #include "sen/core/io/detail/serialization_traits.h" +#include "sen/core/meta/enum_traits.h" +#include "sen/core/meta/native_types.h" +#include "sen/core/meta/optional_traits.h" +#include "sen/core/meta/sequence_traits.h" +#include "sen/core/meta/time_types.h" +#include "sen/core/meta/type_traits.h" + +// generated code +#include "stl/test_struct_traits.stl.h" +#include "stl/test_variant_traits.stl.h" // google test #include +// json +#include + // std +#include +#include #include +#include #include +#include #include #include +#include #include +#include #include using sen::impl::allowsContiguousIO; @@ -27,6 +47,9 @@ using sen::impl::isNumeric; using sen::impl::isPureIntegral; using sen::impl::IsStreamable; +namespace +{ + struct TrivialType { int32_t x; @@ -37,11 +60,40 @@ enum class TestEnum8 : uint8_t { value = 1 }; + enum class TestEnum32 : uint32_t { value = 100 }; +} // namespace + +namespace sen +{ + +template <> +struct StringConversionTraits +{ + static std::string_view toString(TestEnum8 val) + { + if (val == TestEnum8::value) + { + return "value"; + } + return "unknown"; + } + static TestEnum8 fromString(std::string_view val) + { + if (val == "value") + { + return TestEnum8::value; + } + throw std::runtime_error("Invalid enum"); + } +}; + +} // namespace sen + /// @test /// Check pure integral types /// @requirements(SEN-1052) @@ -240,3 +292,208 @@ TEST(SerializationTraits, getSerializedSizeTimeStamp) const sen::TimeStamp ts; EXPECT_EQ(sen::impl::getSerializedSize(ts), sen::impl::getSerializedSize(int64_t {0})); } + +/// @test +/// Checks JSON string conversion for native numeric types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, NativeTypeConversion) +{ + constexpr uint32_t original = 8080U; + const std::string jsonStr = sen::SerializationTraits::toJsonString(original); + + const auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_number_unsigned()); + EXPECT_EQ(jsonObj.get(), 8080U); + + uint32_t recovered = 0U; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for string types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, StringTypeConversion) +{ + const std::string original = "String Test"; + const std::string jsonStr = sen::SerializationTraits::toJsonString(original); + + const auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_string()); + EXPECT_EQ(jsonObj.get(), "String Test"); + + std::string recovered; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for sequence types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, SequenceConversion) +{ + const std::vector original = {256, 512, 1024}; + const std::string jsonStr = sen::SequenceTraitsBase>::toJsonString(original); + + auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_array()); + ASSERT_EQ(jsonObj.size(), 3U); + EXPECT_EQ(jsonObj[0].get(), 256); + EXPECT_EQ(jsonObj[1].get(), 512); + EXPECT_EQ(jsonObj[2].get(), 1024); + + std::vector recovered; + sen::SequenceTraitsBase>::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for Duration type +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, DurationConversion) +{ + const sen::Duration original(5000); + const std::string jsonStr = sen::SerializationTraits::toJsonString(original); + + const auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_number_integer()); + EXPECT_EQ(jsonObj.get(), 5000); + + sen::Duration recovered; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for TimeStamp type +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, TimeStampConversion) +{ + const sen::TimeStamp original(sen::Duration(0)); + const std::string jsonStr = sen::SerializationTraits::toJsonString(original); + + const auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_string()); + EXPECT_EQ(jsonObj.get(), "1970-01-01 00:00:00 000000"); + + sen::TimeStamp recovered; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + + const auto timeDifference = + std::abs(original.sinceEpoch().getNanoseconds() - recovered.sinceEpoch().getNanoseconds()); + constexpr auto maxTimezoneOffset = + std::chrono::duration_cast(std::chrono::hours(24)).count(); + EXPECT_LE(timeDifference, maxTimezoneOffset); +} + +/// @test +/// Checks JSON string conversion for optional types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, OptionalConversion) +{ + constexpr std::optional originalWithValue = 128; + const std::string jsonStrWithValue = sen::OptionalTraitsBase>::toJsonString(originalWithValue); + + auto jsonObjWithValue = nlohmann::json::parse(jsonStrWithValue); + EXPECT_TRUE(jsonObjWithValue.is_number_integer()); + EXPECT_EQ(jsonObjWithValue.get(), 128); + + std::optional recoveredWithValue; + sen::OptionalTraitsBase>::fromJsonString(jsonStrWithValue, recoveredWithValue); + EXPECT_TRUE(recoveredWithValue.has_value()); + EXPECT_EQ(originalWithValue.value(), recoveredWithValue.value()); + + constexpr std::optional originalEmpty = std::nullopt; + const std::string jsonStrEmpty = sen::OptionalTraitsBase>::toJsonString(originalEmpty); + + auto jsonObjEmpty = nlohmann::json::parse(jsonStrEmpty); + EXPECT_TRUE(jsonObjEmpty.is_null()); + + std::optional recoveredEmpty; + sen::OptionalTraitsBase>::fromJsonString(jsonStrEmpty, recoveredEmpty); + EXPECT_FALSE(recoveredEmpty.has_value()); +} + +/// @test +/// Checks JSON string conversion for enum types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, EnumConversion) +{ + constexpr auto original = TestEnum8::value; + const std::string jsonStr = sen::EnumTraitsBase::toJsonString(original); + + const auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_number_unsigned()); + EXPECT_EQ(jsonObj.get(), static_cast(TestEnum8::value)); + + TestEnum8 recovered; + sen::EnumTraitsBase::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for array and sequence types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, ArrayConversion) +{ + constexpr std::array original = {192, 168, 1}; + const std::string jsonStr = sen::ArrayTraitsBase>::toJsonString(original); + + auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_array()); + ASSERT_EQ(jsonObj.size(), 3U); + EXPECT_EQ(jsonObj[1].get(), 168); + + std::array recovered {}; + sen::ArrayTraitsBase>::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for structs +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, StructConversion) +{ + test_struct_traits::MyStructWithNativeFieldsOnly original; + original.field1 = 4096U; + original.field2 = "StructTest"; + original.field3 = 3.14f; + + const std::string jsonStr = + sen::SerializationTraits::toJsonString(original); + + auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_object()); + EXPECT_EQ(jsonObj["field1"].get(), 4096U); + EXPECT_EQ(jsonObj["field2"].get(), "StructTest"); + EXPECT_FLOAT_EQ(jsonObj["field3"].get(), 3.14f); + + test_struct_traits::MyStructWithNativeFieldsOnly recovered; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + + EXPECT_EQ(original.field1, recovered.field1); + EXPECT_EQ(original.field2, recovered.field2); + EXPECT_FLOAT_EQ(original.field3, recovered.field3); +} + +/// @test +/// Checks JSON string conversion for variants +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, VariantConversion) +{ + test_variant_traits::MockVariant original; + original.emplace<2>(9999U); + + const std::string jsonStr = sen::SerializationTraits::toJsonString(original); + + auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_object()); + EXPECT_EQ(jsonObj["type"].get(), 2U); + EXPECT_EQ(jsonObj["value"].get(), 9999U); + + test_variant_traits::MockVariant recovered; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + + ASSERT_EQ(recovered.index(), 2U); + EXPECT_EQ(std::get<2>(recovered), 9999U); +} From 365b30c01e69108ef402a06b4da8e91f0c4c2fce Mon Sep 17 00:00:00 2001 From: Airbus contributor Date: Thu, 21 May 2026 18:12:45 +0200 Subject: [PATCH 14/17] fix(rest): get object definition endpoint returning invalid setters Get object definition endpoint returned all setters, also including the related to not writable properties. It was also not returning the definition endpoints for getters. resolves SEN-1591 --- components/rest/src/sen_router.cpp | 30 ++++++++++---------- components/rest/test/e2e_test.cpp | 45 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/components/rest/src/sen_router.cpp b/components/rest/src/sen_router.cpp index 8c1cf3e6..9e580962 100644 --- a/components/rest/src/sen_router.cpp +++ b/components/rest/src/sen_router.cpp @@ -32,6 +32,7 @@ #include "sen/core/meta/callable.h" #include "sen/core/meta/event.h" #include "sen/core/meta/method.h" +#include "sen/core/meta/property.h" #include "sen/core/meta/var.h" #include "sen/core/obj/callback.h" #include "sen/core/obj/object.h" @@ -171,13 +172,18 @@ Object SenRouter::getObjectDefinition(const std::string& urlPath, const sen::Obj for (const auto& prop: objectClass->getProperties(sen::ClassType::SearchMode::includeParents)) { // prop getters - const std::string getterRelUrl = urlPath + "/methods/" + std::string(prop->getGetterMethod().getName()); + const auto& getter = prop->getGetterMethod(); + std::string getterRelUrl = urlPath + "/methods/" + std::string(getter.getName()); links.emplace_back(Link {RelType::getter, getterRelUrl + "/invoke", HttpMethod::httpPost}); + links.emplace_back(Link {RelType::def, std::move(getterRelUrl), HttpMethod::httpGet}); // prop setters - const std::string setterRelUrl = urlPath + "/methods/" + std::string(prop->getSetterMethod().getName()); - links.emplace_back(Link {RelType::setter, setterRelUrl + "/invoke", HttpMethod::httpPost}); - links.emplace_back(Link {RelType::def, setterRelUrl, HttpMethod::httpGet}); + if (const auto& setter = prop->getSetterMethod(); prop->getCategory() == PropertyCategory::dynamicRW) + { + std::string setterRelUrl = urlPath + "/methods/" + std::string(setter.getName()); + links.emplace_back(Link {RelType::setter, setterRelUrl + "/invoke", HttpMethod::httpPost}); + links.emplace_back(Link {RelType::def, std::move(setterRelUrl), HttpMethod::httpGet}); + } // prop change subscription const std::string propRelUrlEvent = urlPath + "/properties/" + std::string(prop->getName()); @@ -214,17 +220,8 @@ std::optional SenRouter::getMethodDefinition(const InterestSubscri auto objectClass = object->getClass(); - const auto& methods = objectClass->getMethods(sen::ClassType::SearchMode::includeParents); - const auto methodIt = std::find_if(methods.cbegin(), - methods.cend(), - [&methodLocator](const std::shared_ptr methodPtr) - { return methodPtr && methodPtr->getName() == methodLocator.method(); }); - if (methodIt == methods.cend()) - { - return std::nullopt; - } - - auto method = methodIt->get(); + const auto method = + objectClass->searchMethodByName(methodLocator.method(), sen::ClassType::SearchMode::includeParents); if (!method) { return std::nullopt; @@ -854,7 +851,8 @@ JsonResponse SenRouter::invokeMethodHandler(ClientSession& clientSession, return getErrorNotFound(); } - const sen::Method* method = object->getClass()->searchMethodByName(locator.method()); + const sen::Method* method = + object->getClass()->searchMethodByName(locator.method(), sen::ClassType::SearchMode::includeParents); if (!method) { return JsonResponse(httpNotFoundError, Error {"method not found"}); diff --git a/components/rest/test/e2e_test.cpp b/components/rest/test/e2e_test.cpp index 0bf53d8c..20fc8bbc 100644 --- a/components/rest/test/e2e_test.cpp +++ b/components/rest/test/e2e_test.cpp @@ -584,6 +584,51 @@ TEST(Rest, e2e_get_method_definition) ASSERT_EQ(res["name"], "shutdown"); } +/// @test +/// End-to-end test to verify all returned definition links are accessible +/// @requirements(SEN-1061) +TEST(Rest, e2e_get_all_method_definitions) +{ + Server server; + + // Authenticate + auto token = authenticate(); + ASSERT_TRUE(token.has_value()); + + // Create interest + auto createRet = request(HttpMethod::httpPost, + "127.0.0.1", + "12345", + "/api/interests", + Json {{"name", "test_interest"}, {"query", "SELECT * FROM local.kernel"}}, + token.value()); + ASSERT_EQ(createRet.statusCode, 200); + + // Retrieve object definition + HttpResponse ret = retryUntil( + 200, + [&token]() + { + return request( + HttpMethod::httpGet, "127.0.0.1", "12345", "/api/interests/test_interest/objects/api", Json(), token.value()); + }); + ASSERT_EQ(ret.statusCode, 200); + + auto res = Json::parse(ret.body); + ASSERT_TRUE(res.is_object()); + + // Walk all definition links + for (const auto& link: res["links"]) + { + if (link["rel"] != "def") + { + continue; + } + auto defRet = request(HttpMethod::httpGet, "127.0.0.1", "12345", link["href"], Json(), token.value()); + ASSERT_EQ(defRet.statusCode, 200); + } +} + /// @test /// End-to-end test for getting property definition /// @requirements(SEN-1061) From 8ee8dddc16be3ab3a1cc9e5935fe29d1bd839ac5 Mon Sep 17 00:00:00 2001 From: Florian Sattler Date: Tue, 26 May 2026 21:50:05 +0200 Subject: [PATCH 15/17] chore: adds python linting and formatting setup resolves SEN-1688 --- .cmake-format.py | 323 +++++++++--------- .conan/test_packages/package/conanfile.py | 2 +- .pre-commit-config.yaml | 14 + apps/cli_gen/test/test19/test.py | 11 +- apps/cli_gen/test/test20/test.py | 18 +- cmake/util/color_cmake_target_graph.py | 6 +- cmake/util/generate_coverage_report.py | 1 + components/py/test/src/modify_objects.py | 14 +- .../recorder/test/data/gen_crash_record.py | 1 + examples/apps/rest_python/sen_client.py | 47 ++- .../10_python/scripts/creating_objects.py | 17 +- .../config/10_python/scripts/hello_python.py | 2 + .../10_python/scripts/inspecting_objects.py | 4 +- .../scripts/interacting_with_objects.py | 2 +- .../6_recorder/3_recorder_school_print.py | 16 +- .../crash_report/crash_report_tester.py | 9 +- libs/kernel/test/integration/runner.py | 9 +- libs/kernel/test/integration/tester.py | 17 +- .../type_clash/type_clash_tester.py | 4 + ruff.toml | 34 ++ 20 files changed, 313 insertions(+), 238 deletions(-) create mode 100644 ruff.toml diff --git a/.cmake-format.py b/.cmake-format.py index 1037c5fd..93b4fed0 100644 --- a/.cmake-format.py +++ b/.cmake-format.py @@ -10,137 +10,156 @@ # ---------------------------------- with section("parse"): # Specify structure for custom cmake functions - additional_commands = {'add_sen_integration_test': {'kwargs': {'WORKING_DIRECTORY': 1, - 'REQ_COMPONENTS': 3, - 'REQ_DEPS': 3}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'add_sen_run_smoke_test': {'kwargs': {'NAME': 1, - 'CONFIG_FILE': 1, - 'WORKING_DIRECTORY': 1}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'add_sen_cli_gen_smoke_test': {'kwargs': {'NAME': 1, - 'COMMAND': 1, - 'WORKING_DIRECTORY': 1}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'add_sen_smoke_test': {'kwargs': {'NAME': 1, - 'COMMAND': 1, - 'WORKING_DIRECTORY': 1}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'sen_add_dependency': {'pargs': {'nargs': 3}}, - 'sen_configure_target': {'pargs': {'nargs': 1}}, - 'sen_enable_static_analysis': {'pargs': {'nargs': 1}}, - 'sen_generate_code': {'kwargs': {'TARGET': 1, - 'OUTPUT_DIR': 1, - 'BASE_PATH': 1, - 'LANG': 1, - 'CODEGEN_SETTINGS': 1, - 'GEN_HDR_FILES': 1, - 'SCHEMA_FILE': 1, - 'SCHEMA_COMPONENT_NAME': 1, - 'HLA_OUTPUT_DIR': 1, - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+'}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'sen_generate_cpp': {'kwargs': {'TARGET': 1, - 'OUTPUT_DIR': 1, - 'BASE_PATH': 1, - 'LANG': 1, - 'CODEGEN_SETTINGS': 1, - 'GEN_HDR_FILES': 1, - 'SCHEMA_FILE': 1, - 'SCHEMA_COMPONENT_NAME': 1, - 'HLA_OUTPUT_DIR': 1, - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+'}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'sen_generate_python': {'kwargs': {'TARGET': 1, - 'OUTPUT_DIR': 1, - 'BASE_PATH': 1, - 'LANG': 1, - 'CODEGEN_SETTINGS': 1, - 'GEN_HDR_FILES': 1, - 'SCHEMA_FILE': 1, - 'SCHEMA_COMPONENT_NAME': 1, - 'HLA_OUTPUT_DIR': 1, - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+'}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'sen_generate_uml': {'kwargs': {'BASE_PATH': 1, - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'OUT': 1}, - 'pargs': {'flags': ['CLASSES_ONLY', - 'TYPES_ONLY', - 'TYPES_ONLY_NO_ENUMS'], - 'nargs': '*'}}, - 'add_sen_package': {'kwargs': {'TARGET': 1, - 'MAINTAINER': 1, - 'DESCRIPTION': 1, - 'VERSION': 1, - 'BASE_PATH': 1, - 'EXPORT_NAME': 1, - 'SCHEMA_PATH': 1, - 'GEN_HDR_FILES': 1, - 'CODEGEN_SETTINGS': 1, - 'HLA_OUTPUT_DIR': 1, - 'TEST_TARGET': 1, - 'SOURCES': '+', - 'DEPS': '+', - 'PRIVATE_DEPS': '+', - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+', - 'EXPORTED_CLASSES': '+'}, - 'pargs': {'flags': ['NO_SCHEMA', 'IS_COMPONENT', 'PUBLIC_SYMBOLS'], 'nargs': '*'}}, - 'sen_generate_package': {'kwargs': {'TARGET': 1, - 'MAINTAINER': 1, - 'DESCRIPTION': 1, - 'VERSION': 1, - 'BASE_PATH': 1, - 'EXPORT_NAME': 1, - 'SCHEMA_PATH': 1, - 'GEN_HDR_FILES': 1, - 'CODEGEN_SETTINGS': 1, - 'HLA_OUTPUT_DIR': 1, - 'TEST_TARGET': 1, - 'SOURCES': '+', - 'DEPS': '+', - 'PRIVATE_DEPS': '+', - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+', - 'EXPORTED_CLASSES': '+'}, - 'pargs': {'flags': ['NO_SCHEMA', 'IS_COMPONENT'], 'nargs': '*'}}, - 'add_sen_component': {'kwargs': {'TARGET': 1, - 'MAINTAINER': 1, - 'DESCRIPTION': 1, - 'VERSION': 1, - 'BASE_PATH': 1, - 'EXPORT_NAME': 1, - 'SCHEMA_PATH': 1, - 'GEN_HDR_FILES': 1, - 'CODEGEN_SETTINGS': 1, - 'HLA_OUTPUT_DIR': 1, - 'TEST_TARGET': 1, - 'SOURCES': '+', - 'DEPS': '+', - 'PRIVATE_DEPS': '+', - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+', - 'EXPORTED_CLASSES': '+'}, - 'pargs': {'flags': ['NO_SCHEMA', 'PUBLIC_SYMBOLS'], 'nargs': '*'}}, - 'sen_combine_schemas': {'kwargs': {'SCHEMAS': '+', - 'OUTPUT': 1}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'sen_generate_yaml': {'kwargs': {'DEPS': '+', - 'OUTPUT': 1, - 'SCRIPT': 1, - 'TARGET': 1}, - 'pargs': {'flags': [], 'nargs': '*'}}} + additional_commands = { + "add_sen_integration_test": { + "kwargs": {"WORKING_DIRECTORY": 1, "REQ_COMPONENTS": 3, "REQ_DEPS": 3}, + "pargs": {"flags": [], "nargs": "*"}, + }, + "add_sen_run_smoke_test": { + "kwargs": {"NAME": 1, "CONFIG_FILE": 1, "WORKING_DIRECTORY": 1}, + "pargs": {"flags": [], "nargs": "*"}, + }, + "add_sen_cli_gen_smoke_test": { + "kwargs": {"NAME": 1, "COMMAND": 1, "WORKING_DIRECTORY": 1}, + "pargs": {"flags": [], "nargs": "*"}, + }, + "add_sen_smoke_test": { + "kwargs": {"NAME": 1, "COMMAND": 1, "WORKING_DIRECTORY": 1}, + "pargs": {"flags": [], "nargs": "*"}, + }, + "sen_add_dependency": {"pargs": {"nargs": 3}}, + "sen_configure_target": {"pargs": {"nargs": 1}}, + "sen_enable_static_analysis": {"pargs": {"nargs": 1}}, + "sen_generate_code": { + "kwargs": { + "TARGET": 1, + "OUTPUT_DIR": 1, + "BASE_PATH": 1, + "LANG": 1, + "CODEGEN_SETTINGS": 1, + "GEN_HDR_FILES": 1, + "SCHEMA_FILE": 1, + "SCHEMA_COMPONENT_NAME": 1, + "HLA_OUTPUT_DIR": 1, + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + }, + "pargs": {"flags": [], "nargs": "*"}, + }, + "sen_generate_cpp": { + "kwargs": { + "TARGET": 1, + "OUTPUT_DIR": 1, + "BASE_PATH": 1, + "LANG": 1, + "CODEGEN_SETTINGS": 1, + "GEN_HDR_FILES": 1, + "SCHEMA_FILE": 1, + "SCHEMA_COMPONENT_NAME": 1, + "HLA_OUTPUT_DIR": 1, + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + }, + "pargs": {"flags": [], "nargs": "*"}, + }, + "sen_generate_python": { + "kwargs": { + "TARGET": 1, + "OUTPUT_DIR": 1, + "BASE_PATH": 1, + "LANG": 1, + "CODEGEN_SETTINGS": 1, + "GEN_HDR_FILES": 1, + "SCHEMA_FILE": 1, + "SCHEMA_COMPONENT_NAME": 1, + "HLA_OUTPUT_DIR": 1, + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + }, + "pargs": {"flags": [], "nargs": "*"}, + }, + "sen_generate_uml": { + "kwargs": {"BASE_PATH": 1, "STL_FILES": "+", "HLA_FOM_DIRS": "+", "OUT": 1}, + "pargs": {"flags": ["CLASSES_ONLY", "TYPES_ONLY", "TYPES_ONLY_NO_ENUMS"], "nargs": "*"}, + }, + "add_sen_package": { + "kwargs": { + "TARGET": 1, + "MAINTAINER": 1, + "DESCRIPTION": 1, + "VERSION": 1, + "BASE_PATH": 1, + "EXPORT_NAME": 1, + "SCHEMA_PATH": 1, + "GEN_HDR_FILES": 1, + "CODEGEN_SETTINGS": 1, + "HLA_OUTPUT_DIR": 1, + "TEST_TARGET": 1, + "SOURCES": "+", + "DEPS": "+", + "PRIVATE_DEPS": "+", + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + "EXPORTED_CLASSES": "+", + }, + "pargs": {"flags": ["NO_SCHEMA", "IS_COMPONENT", "PUBLIC_SYMBOLS"], "nargs": "*"}, + }, + "sen_generate_package": { + "kwargs": { + "TARGET": 1, + "MAINTAINER": 1, + "DESCRIPTION": 1, + "VERSION": 1, + "BASE_PATH": 1, + "EXPORT_NAME": 1, + "SCHEMA_PATH": 1, + "GEN_HDR_FILES": 1, + "CODEGEN_SETTINGS": 1, + "HLA_OUTPUT_DIR": 1, + "TEST_TARGET": 1, + "SOURCES": "+", + "DEPS": "+", + "PRIVATE_DEPS": "+", + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + "EXPORTED_CLASSES": "+", + }, + "pargs": {"flags": ["NO_SCHEMA", "IS_COMPONENT"], "nargs": "*"}, + }, + "add_sen_component": { + "kwargs": { + "TARGET": 1, + "MAINTAINER": 1, + "DESCRIPTION": 1, + "VERSION": 1, + "BASE_PATH": 1, + "EXPORT_NAME": 1, + "SCHEMA_PATH": 1, + "GEN_HDR_FILES": 1, + "CODEGEN_SETTINGS": 1, + "HLA_OUTPUT_DIR": 1, + "TEST_TARGET": 1, + "SOURCES": "+", + "DEPS": "+", + "PRIVATE_DEPS": "+", + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + "EXPORTED_CLASSES": "+", + }, + "pargs": {"flags": ["NO_SCHEMA", "PUBLIC_SYMBOLS"], "nargs": "*"}, + }, + "sen_combine_schemas": {"kwargs": {"SCHEMAS": "+", "OUTPUT": 1}, "pargs": {"flags": [], "nargs": "*"}}, + "sen_generate_yaml": { + "kwargs": {"DEPS": "+", "OUTPUT": 1, "SCRIPT": 1, "TARGET": 1}, + "pargs": {"flags": [], "nargs": "*"}, + }, + } # Override configurations per-command where available override_spec = {} @@ -175,7 +194,7 @@ # 'use-space', fractional indentation is left as spaces (utf-8 0x20). If set # to `round-up` fractional indentation is replaced with a single tab character # (utf-8 0x09) effectively shifting the column to the next tabstop - fractional_tab_policy = 'use-space' + fractional_tab_policy = "use-space" # If an argument group contains more than this many sub-groups (parg or kwarg # groups) then force it to a vertical layout. @@ -203,7 +222,7 @@ # to this reference: `prefix`: the start of the statement, `prefix-indent`: # the start of the statement, plus one indentation level, `child`: align to # the column of the arguments - dangle_align = 'prefix' + dangle_align = "prefix" # If the statement spelling length (including space and parenthesis) is # smaller than this amount, then force reject nested layouts. @@ -219,13 +238,13 @@ max_lines_hwrap = 2 # What style line endings to use in the output. - line_ending = 'unix' + line_ending = "unix" # Format command names consistently as 'lower' or 'upper' case - command_case = 'canonical' + command_case = "canonical" # Format keywords consistently as 'lower' or 'upper' case - keyword_case = 'upper' + keyword_case = "upper" # A list of command names which should always be wrapped always_wrap = [] @@ -253,10 +272,10 @@ # ------------------------------------------------ with section("markup"): # What character to use for bulleted lists - bullet_char = '-' + bullet_char = "-" # What character to use as punctuation after numerals in an enumerated list - enum_char = '.' + enum_char = "." # If comment markup is enabled, don't reflow the first comment block in each # listfile. Use this to preserve formatting of your copyright/license @@ -269,15 +288,15 @@ # Regular expression to match preformat fences in comments default= # ``r'^\s*([`~]{3}[`~]*)(.*)$'`` - fence_pattern = '^\\s*([`~]{3}[`~]*)(.*)$' + fence_pattern = "^\\s*([`~]{3}[`~]*)(.*)$" # Regular expression to match rulers in comments default= # ``r'^\s*[^\w\s]{3}.*[^\w\s]{3}$'`` - ruler_pattern = '^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$' + ruler_pattern = "^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$" # If a comment line matches starts with this pattern then it is explicitly a # trailing comment for the preceeding argument. Default is '#<' - explicit_trailing_pattern = '#<' + explicit_trailing_pattern = "#<" # If a comment line starts with at least this many consecutive hash # characters, then don't lstrip() them off. This allows for lazy hash rulers @@ -299,38 +318,38 @@ disabled_codes = [] # regular expression pattern describing valid function names - function_pattern = '[0-9a-z_]+' + function_pattern = "[0-9a-z_]+" # regular expression pattern describing valid macro names - macro_pattern = '[0-9A-Z_]+' + macro_pattern = "[0-9A-Z_]+" # regular expression pattern describing valid names for variables with global # (cache) scope - global_var_pattern = '[A-Z][0-9A-Z_]+' + global_var_pattern = "[A-Z][0-9A-Z_]+" # regular expression pattern describing valid names for variables with global # scope (but internal semantic) - internal_var_pattern = '_[A-Z][0-9A-Z_]+' + internal_var_pattern = "_[A-Z][0-9A-Z_]+" # regular expression pattern describing valid names for variables with local # scope - local_var_pattern = '[a-z][a-z0-9_]+' + local_var_pattern = "[a-z][a-z0-9_]+" # regular expression pattern describing valid names for privatedirectory # variables - private_var_pattern = '_[0-9a-z_]+' + private_var_pattern = "_[0-9a-z_]+" # regular expression pattern describing valid names for public directory # variables - public_var_pattern = '[A-Z][0-9A-Z_]+' + public_var_pattern = "[A-Z][0-9A-Z_]+" # regular expression pattern describing valid names for function/macro # arguments and loop variables. - argument_var_pattern = '[a-z][a-z0-9_]+' + argument_var_pattern = "[a-z][a-z0-9_]+" # regular expression pattern describing valid names for keywords used in # functions or macros - keyword_pattern = '[A-Z][0-9A-Z_]+' + keyword_pattern = "[A-Z][0-9A-Z_]+" # In the heuristic for C0201, how many conditionals to match within a loop in # before considering the loop a parser. @@ -355,11 +374,11 @@ emit_byteorder_mark = False # Specify the encoding of the input file. Defaults to utf-8 - input_encoding = 'utf-8' + input_encoding = "utf-8" # Specify the encoding of the output file. Defaults to utf-8. Note that cmake # only claims to support utf-8 so be careful when using anything else - output_encoding = 'utf-8' + output_encoding = "utf-8" # ------------------------------------- # Miscellaneous configurations options. diff --git a/.conan/test_packages/package/conanfile.py b/.conan/test_packages/package/conanfile.py index dcaa6699..28e55ca1 100644 --- a/.conan/test_packages/package/conanfile.py +++ b/.conan/test_packages/package/conanfile.py @@ -1,4 +1,3 @@ - # === conanfile.py ===================================================================================================== # Sen Infrastructure # Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). @@ -12,6 +11,7 @@ from conan.tools.cmake import CMakeToolchain, CMake from conan.tools.build import cross_building + class TestPackageConan(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeDeps", "VirtualRunEnv" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43048800..25b28e1d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,5 +38,19 @@ repos: hooks: - id: cmake-format +- repo: https://github.com/jackdewinter/pymarkdown + rev: v0.9.20 + hooks: + - id: pymarkdown + name: Checking markdown files + args: [--config, .pymarkdown.yaml, fix] + +- repo: https://github.com/astral-sh/ruff-pre-commit.git + rev: v0.15.14 + hooks: + # - id: ruff + # args: [--fix] + - id: ruff-format + # Global Configuration exclude: (examples/packages/hla_fom/.*|test/util/dummy_entities/hla/.*|.*.svg|.*.excalidraw|stl.tmLanguage.json) diff --git a/apps/cli_gen/test/test19/test.py b/apps/cli_gen/test/test19/test.py index 4720b6e1..98f1100d 100755 --- a/apps/cli_gen/test/test19/test.py +++ b/apps/cli_gen/test/test19/test.py @@ -11,20 +11,13 @@ # integration test to check that the codegen settings work well in FOM code generation if __name__ == "__main__": - # input arguments exe_path = "./cli_gen" source_dir = os.path.dirname(os.path.abspath(__file__)) output_file = os.path.join(os.getcwd(), "fom", "modulea-19.xml.h") target_word = "moduleAIntAcceptsSet" - cmd = [ - exe_path, - "cpp", - "fom", - f"--directories={source_dir}/fom", - f"--settings={source_dir}/codegen_settings.json" - ] + cmd = [exe_path, "cpp", "fom", f"--directories={source_dir}/fom", f"--settings={source_dir}/codegen_settings.json"] print(f"Executing: {' '.join(cmd)}") @@ -40,7 +33,7 @@ print(f"Error: Generated file NOT FOUND at {output_file}") sys.exit(1) - with open(output_file, 'r') as f: + with open(output_file, "r") as f: if target_word in f.read(): # the test passes if the skeleton method for the checked property is present print(f"Success: Found '{target_word}' in {output_file}") diff --git a/apps/cli_gen/test/test20/test.py b/apps/cli_gen/test/test20/test.py index 8ab3939d..eb880333 100755 --- a/apps/cli_gen/test/test20/test.py +++ b/apps/cli_gen/test/test20/test.py @@ -11,7 +11,6 @@ # integration test to check that the FOM generator creates Maybe types for optional parameters if __name__ == "__main__": - # input arguments exe_path = "./cli_gen" source_dir = os.path.dirname(os.path.abspath(__file__)) @@ -20,12 +19,7 @@ target_optional = "MaybeI32 myOptionalParam" target_required = "IntModuleA myRequiredParam" - cmd = [ - exe_path, - "cpp", - "fom", - f"--directories={source_dir}/fom" - ] + cmd = [exe_path, "cpp", "fom", f"--directories={source_dir}/fom"] print(f"Executing: {' '.join(cmd)}") @@ -41,15 +35,19 @@ print(f"Error: Generated file NOT FOUND at {output_file}") sys.exit(1) - with open(output_file, 'r') as f: + with open(output_file, "r") as f: content = f.read() if target_optional not in content: - print(f"Failure: Optional parameter generation failed. '{target_optional}' not found in the file {output_file}.") + print( + f"Failure: Optional parameter generation failed. '{target_optional}' not found in the file {output_file}." + ) sys.exit(1) if target_required not in content: - print(f"Failure: Required parameter generation failed. '{target_required}' not found in the file {output_file}.") + print( + f"Failure: Required parameter generation failed. '{target_required}' not found in the file {output_file}." + ) sys.exit(1) print(f"Success: Found correctly generated parameters in {output_file}") diff --git a/cmake/util/color_cmake_target_graph.py b/cmake/util/color_cmake_target_graph.py index a253a387..fc25e609 100755 --- a/cmake/util/color_cmake_target_graph.py +++ b/cmake/util/color_cmake_target_graph.py @@ -17,10 +17,10 @@ "hexagon": {"outline": "#e57373", "fill": "#ef9a9a"}, # object libs "tripleoctagon": {"outline": "#fb8c00", "fill": "#ffcc80"}, # module libs "pentagon": {"outline": "#4caf50", "fill": "#81c784"}, # interface libs - "box": {"outline": "#66bb6a", "fill": "#a5d6a7"} # custom target + "box": {"outline": "#66bb6a", "fill": "#a5d6a7"}, # custom target } -shape_re = re.compile(r'(shape\s*=\s*\"?([a-zA-z0-9_]+)\"?)') +shape_re = re.compile(r"(shape\s*=\s*\"?([a-zA-z0-9_]+)\"?)") with open(input_path, "r", encoding="utf-8") as f: lines = f.readlines() @@ -36,7 +36,7 @@ if data: color = data.get("outline") fillcolor = data.get("fill") - line = line.replace(']', f', color="{color}", fillcolor="{fillcolor}"]', 1) + line = line.replace("]", f', color="{color}", fillcolor="{fillcolor}"]', 1) out_lines.append(line) with open(output_path, "w", encoding="utf-8") as f: diff --git a/cmake/util/generate_coverage_report.py b/cmake/util/generate_coverage_report.py index 651852f6..8e1fad00 100755 --- a/cmake/util/generate_coverage_report.py +++ b/cmake/util/generate_coverage_report.py @@ -88,6 +88,7 @@ def generate_coverage_report(llvm_cov, profile, report_dir, binaries, ignore_reg stdout=output_file, ) + def generate_coverage_summary(llvm_cov, profile, report_dir, binaries, ignore_regex: tp.Optional[str]): """Generates a coverage summary for the given coverage data.""" print(" ├ Generating coverage summary...") diff --git a/components/py/test/src/modify_objects.py b/components/py/test/src/modify_objects.py index a98755cd..903304b9 100644 --- a/components/py/test/src/modify_objects.py +++ b/components/py/test/src/modify_objects.py @@ -15,23 +15,29 @@ static_value = 56 dynamic_value = 567 + def dynamic_prop_changed(): global test_object, dynamic_value, cb_fired - assert test_object.dynamicProp == dynamic_value, f"Error in dynamicProp [value: {test_object.dynamicProp}, expectation: {dynamic_value}]" + assert test_object.dynamicProp == dynamic_value, ( + f"Error in dynamicProp [value: {test_object.dynamicProp}, expectation: {dynamic_value}]" + ) # stopping after checking the value of the property sen.api.requestKernelStop() + def run(): global test_object, test_bus, dynamic_value, static_value - test_object = sen.api.make("py_test_package.TestObject", "test_object", staticProp = static_value) + test_object = sen.api.make("py_test_package.TestObject", "test_object", staticProp=static_value) test_bus = sen.api.getBus("my.tutorial") test_bus.add(test_object) # check the value of the static property - assert test_object.staticProp == static_value, f"Error in staticProp [value: {test_object.staticProp}, expectation: {static_value}]" + assert test_object.staticProp == static_value, ( + f"Error in staticProp [value: {test_object.staticProp}, expectation: {static_value}]" + ) # react to changes in the dynamicProp test_object.onDynamicPropChanged(dynamic_prop_changed) @@ -39,6 +45,7 @@ def run(): # set the dynamic prop to a known value test_object.dynamicProp = dynamic_value + def update(): global cycle @@ -47,6 +54,7 @@ def update(): sen.api.requestKernelStop(1) cycle += 1 + def stop(): global test_bus, test_object diff --git a/components/recorder/test/data/gen_crash_record.py b/components/recorder/test/data/gen_crash_record.py index 14a22cbd..3dce47a0 100644 --- a/components/recorder/test/data/gen_crash_record.py +++ b/components/recorder/test/data/gen_crash_record.py @@ -13,6 +13,7 @@ obj = None execution_counter = 0 + def run(): global obj # refer to the global variable defined above obj = sen.api.open("SELECT * FROM my.tutorial") diff --git a/examples/apps/rest_python/sen_client.py b/examples/apps/rest_python/sen_client.py index e3cb6543..1c7f948e 100644 --- a/examples/apps/rest_python/sen_client.py +++ b/examples/apps/rest_python/sen_client.py @@ -17,6 +17,7 @@ from typing import List, Dict, Any + class SenClient: """ Represents the client for the Sen REST component. @@ -29,7 +30,7 @@ class SenClient: """ def __init__(self, base_url="http://localhost"): - self.base_url = base_url.rstrip('/') + self.base_url = base_url.rstrip("/") self.session = requests.Session() self.sse_event_types = [ "object_added", @@ -46,7 +47,9 @@ def get_headers(self, is_sse=False): Args: is_sse (bool): Whether notifications are enabled. """ - header = {"Content-Type": "application/json", } + header = { + "Content-Type": "application/json", + } if is_sse: header["Accept"] = "text/event-stream" else: @@ -69,11 +72,8 @@ def get(self, href): requests.RequestException: If there is any error in the request. """ try: - url = href if href.startswith('http') else f"{self.base_url}{href}" - response = self.session.get( - url, - headers=self.get_headers() - ) + url = href if href.startswith("http") else f"{self.base_url}{href}" + response = self.session.get(url, headers=self.get_headers()) response.raise_for_status() return response.json() @@ -94,11 +94,8 @@ def post(self, href): requests.RequestException: If there is any error in the request. """ try: - url = href if href.startswith('http') else f"{self.base_url}{href}" - response = self.session.post( - url, - headers=self.get_headers() - ) + url = href if href.startswith("http") else f"{self.base_url}{href}" + response = self.session.post(url, headers=self.get_headers()) response.raise_for_status() return response.json() @@ -120,12 +117,8 @@ def post_json(self, href, body): requests.RequestException: If there is any error in the request. """ try: - url = href if href.startswith('http') else f"{self.base_url}{href}" - response = self.session.post( - url, - headers=self.get_headers(), - json=body - ) + url = href if href.startswith("http") else f"{self.base_url}{href}" + response = self.session.post(url, headers=self.get_headers(), json=body) response.raise_for_status() return response.json() @@ -150,7 +143,7 @@ def post_args(self, href, args_str): if args_str: args = [] - for v in args_str.split(','): + for v in args_str.split(","): v = v.strip() try: args.append(int(v)) @@ -340,19 +333,19 @@ def print_object(self, data): properties = [] events = [] - for link in data.get('links', []): - rel = link.get('rel') - href = link.get('href') + for link in data.get("links", []): + rel = link.get("rel") + href = link.get("href") - if rel == 'method': + if rel == "method": methods.append(href) - elif rel == 'def': + elif rel == "def": methods.append(f"{href} (definition)") - elif rel == 'property': + elif rel == "property": properties.append(href) - elif rel == 'property_subscribe': + elif rel == "property_subscribe": properties.append(f"{href} (subscribe)") - elif rel == 'property_unsubscribe': + elif rel == "property_unsubscribe": properties.append(f"{href} (unsubscribe)") print("\n=== Object Details ===") diff --git a/examples/config/10_python/scripts/creating_objects.py b/examples/config/10_python/scripts/creating_objects.py index 86cf3108..9c64e345 100644 --- a/examples/config/10_python/scripts/creating_objects.py +++ b/examples/config/10_python/scripts/creating_objects.py @@ -9,6 +9,7 @@ myObject = testBus = None + def run(): global myObject, testBus # refer to the globals defined above @@ -19,28 +20,26 @@ def run(): "category": 1, "subcategory": 3, "specific": 0, - "extra": 0 + "extra": 0, } - id = { - "entityNumber": 1, - "federateIdentifier": { - "siteID": 1, - "applicationID": 1 - } - } + id = {"entityNumber": 1, "federateIdentifier": {"siteID": 1, "applicationID": 1}} print(f"Python: creating and publishing the object") - myObject = sen.api.make("aircrafts.DummyAircraft", "myAircraft", entityType=type, alternateEntityType=type, entityIdentifier = id) + myObject = sen.api.make( + "aircrafts.DummyAircraft", "myAircraft", entityType=type, alternateEntityType=type, entityIdentifier=id + ) testBus = sen.api.getBus("my.tutorial") testBus.add(myObject) # setting the speed property to 150 myObject.speed = 150 + def update(): print(myObject) + def stop(): global testBus, myObject # refer to the globals defined above diff --git a/examples/config/10_python/scripts/hello_python.py b/examples/config/10_python/scripts/hello_python.py index 3a8951f2..2affaf1c 100644 --- a/examples/config/10_python/scripts/hello_python.py +++ b/examples/config/10_python/scripts/hello_python.py @@ -7,6 +7,7 @@ import sen + # this is executed only once (at the start of the component execution) def run(): print(f"Python: run") @@ -18,6 +19,7 @@ def run(): def update(): print(f"Python: update (current time: {sen.api.time})") + # this is executed only once (at the end of the component execution) def stop(): print(f"Python: stop called") diff --git a/examples/config/10_python/scripts/inspecting_objects.py b/examples/config/10_python/scripts/inspecting_objects.py index 314b28ee..34b4602d 100644 --- a/examples/config/10_python/scripts/inspecting_objects.py +++ b/examples/config/10_python/scripts/inspecting_objects.py @@ -17,8 +17,8 @@ def run(): list = sen.api.open("SELECT * FROM local.kernel") # open it # register some callbacks to show changes in the list - list.onAdded(lambda obj: print(f'Python: object added {obj}')) - list.onRemoved(lambda obj: print(f'Python: object removed {obj}')) + list.onAdded(lambda obj: print(f"Python: object added {obj}")) + list.onRemoved(lambda obj: print(f"Python: object removed {obj}")) def update(): diff --git a/examples/config/10_python/scripts/interacting_with_objects.py b/examples/config/10_python/scripts/interacting_with_objects.py index 6f4ef895..6ed10801 100644 --- a/examples/config/10_python/scripts/interacting_with_objects.py +++ b/examples/config/10_python/scripts/interacting_with_objects.py @@ -13,7 +13,7 @@ def run(): global obj # refer to the global variable defined above - obj = sen.api.open("SELECT * FROM local.shell WHERE name = \"shell_impl\"") + obj = sen.api.open('SELECT * FROM local.shell WHERE name = "shell_impl"') def update(): diff --git a/examples/config/6_recorder/3_recorder_school_print.py b/examples/config/6_recorder/3_recorder_school_print.py index 97b2380c..0d36ab5c 100644 --- a/examples/config/6_recorder/3_recorder_school_print.py +++ b/examples/config/6_recorder/3_recorder_school_print.py @@ -8,13 +8,13 @@ import sen_db_python as sen from datetime import datetime -epoch = datetime(1970,1,1) +epoch = datetime(1970, 1, 1) try: input = sen.Input("school_recording") print(f"Opened archive '{input.path}' with the following summary:") - print(f" - firstTime: {epoch+input.summary.firstTime}") - print(f" - lastTime: {epoch+input.summary.lastTime}") + print(f" - firstTime: {epoch + input.summary.firstTime}") + print(f" - lastTime: {epoch + input.summary.lastTime}") print(f" - keyframeCount: {input.summary.keyframeCount}") print(f" - objectCount: {input.summary.objectCount}") print(f" - typeCount: {input.summary.typeCount}") @@ -30,31 +30,31 @@ entry = cursor.entry if type(entry.payload) is sen.Keyframe: - print(f"{epoch+entry.time} -> Keyframe:") + print(f"{epoch + entry.time} -> Keyframe:") for obj in entry.payload.snapshots: print(f" - object: {obj.name}") print(f" class: {obj.className}") print("") elif type(entry.payload) is sen.Creation: - print(f"{epoch+entry.time} -> Object Creation:") + print(f"{epoch + entry.time} -> Object Creation:") print(f" - name: {entry.payload.name}") print(f" - class: {entry.payload.className}") print("") elif type(entry.payload) is sen.Deletion: - print(f"{epoch+entry.time} -> Object Deletion:") + print(f"{epoch + entry.time} -> Object Deletion:") print(f" - object id: {entry.payload.objectId}") print("") elif type(entry.payload) is sen.PropertyChange: - print(f"{epoch+entry.time} -> Property Changed:") + print(f"{epoch + entry.time} -> Property Changed:") print(f" - object id: {entry.payload.objectId}") print(f" - property: {entry.payload.name}") print("") elif type(entry.payload) is sen.Event: - print(f"{epoch+entry.time} -> Event:") + print(f"{epoch + entry.time} -> Event:") print(f" - object id: {entry.payload.objectId}") print(f" - event: {entry.payload.name}") print("") diff --git a/libs/kernel/test/integration/crash_report/crash_report_tester.py b/libs/kernel/test/integration/crash_report/crash_report_tester.py index 81f877ec..7209425e 100644 --- a/libs/kernel/test/integration/crash_report/crash_report_tester.py +++ b/libs/kernel/test/integration/crash_report/crash_report_tester.py @@ -11,11 +11,14 @@ import re import platform + def test_crash_reporter_generates_stacktrace(): config_path = os.path.join(os.path.dirname(__file__), "config", "config.yaml") - sen_executable = 'sen' if platform.system() == 'Windows' else './sen' + sen_executable = "sen" if platform.system() == "Windows" else "./sen" - process = subprocess.Popen([sen_executable, 'run', config_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + process = subprocess.Popen( + [sen_executable, "run", config_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) _, stderr = process.communicate() match = re.search(r"Crash report written to (.*\.json)", stderr) @@ -25,7 +28,7 @@ def test_crash_reporter_generates_stacktrace(): assert os.path.exists(report_path), f"Crash report file does not exist: {report_path}" try: - with open(report_path, 'r') as f: + with open(report_path, "r") as f: data = json.load(f) error_data = data.get("errorData", {}) diff --git a/libs/kernel/test/integration/runner.py b/libs/kernel/test/integration/runner.py index a69412e0..a73151ee 100644 --- a/libs/kernel/test/integration/runner.py +++ b/libs/kernel/test/integration/runner.py @@ -10,11 +10,12 @@ import os from time import sleep + def run_sen_command(arg): - if os.name == 'nt': # Windows - subprocess.Popen(['sen', 'run', arg], start_new_session=True, env=os.environ.copy()) + if os.name == "nt": # Windows + subprocess.Popen(["sen", "run", arg], start_new_session=True, env=os.environ.copy()) else: # Unix-like - subprocess.Popen(['./sen', 'run', arg], start_new_session=True) + subprocess.Popen(["./sen", "run", arg], start_new_session=True) def main(): @@ -31,7 +32,7 @@ def main(): run_sen_command(arg2) # Run the main instance for the smoke test - os.execv(os.path.join(os.curdir, "sen"), ['sen', 'run', arg3]) + os.execv(os.path.join(os.curdir, "sen"), ["sen", "run", arg3]) if __name__ == "__main__": diff --git a/libs/kernel/test/integration/tester.py b/libs/kernel/test/integration/tester.py index ff411f3d..cb375602 100644 --- a/libs/kernel/test/integration/tester.py +++ b/libs/kernel/test/integration/tester.py @@ -10,6 +10,7 @@ import sen from datetime import datetime + class TesterBase(ABC): def __init__(self, name, sen_api): """ @@ -58,8 +59,9 @@ def execute_test(self, test_name): Execute a given test if it hasn't been run already and its condition has been met. Store test result inside the test dictionary and manage the assertion error in case the test fails. """ - if (not self.__have_tests_run.get(test_name) and - self.__test_conditions[test_name]()): # Test hasn't been run and its condition is met + if ( + not self.__have_tests_run.get(test_name) and self.__test_conditions[test_name]() + ): # Test hasn't been run and its condition is met try: self.__tests[test_name]() # Execute the test function self.__have_tests_run[test_name] = True # Mark as executed @@ -77,11 +79,11 @@ def run_tests(self): if self.__have_tests_failed[test_name]: self.mark_as_failed() + class KernelTransportTester(TesterBase): def set_tests(self): - def test_condition(): - " Check that both tester objects are ready prior to executing the test" + "Check that both tester objects are ready prior to executing the test" global object_list, tester1, tester2 expected_names = {"tester1", "tester2", "obj1", "obj2"} @@ -90,7 +92,9 @@ def test_condition(): tester2 = next((obj for obj in object_list if obj.name == "tester2"), None) testers_ready = tester1 is not None and tester2 is not None and tester1.ready and tester2.ready - print(f"[tester] checking test condition: objects_present: {objects_present} , testers_ready {testers_ready}") + print( + f"[tester] checking test condition: objects_present: {objects_present} , testers_ready {testers_ready}" + ) if objects_present and not testers_ready: print(f"tester1: {tester1.ready}") @@ -135,7 +139,7 @@ def check_test_1(result): tester2.checkLocalState(lambda args: check_test_2(args)) def test_body(): - """ Check setting of remote properties between two kernel instances """ + """Check setting of remote properties between two kernel instances""" global tester1 # test setting remote properties in the forward direction @@ -151,6 +155,7 @@ def test_body(): tester1 = None tester2 = None + def run(): global tester, object_list diff --git a/libs/kernel/test/integration/type_clash/type_clash_tester.py b/libs/kernel/test/integration/type_clash/type_clash_tester.py index 7c32b850..b38cf8dd 100644 --- a/libs/kernel/test/integration/type_clash/type_clash_tester.py +++ b/libs/kernel/test/integration/type_clash/type_clash_tester.py @@ -8,6 +8,7 @@ import sen from tester import TesterBase + class TypeClashTester(TesterBase): def set_tests(self): def test_condition(): @@ -27,15 +28,18 @@ def test_body(): self.set_test("clash_test", test_body, test_condition) + tester = None object_list = None + def run(): global tester, object_list object_list = sen.api.open("SELECT * FROM session.bus") tester = TypeClashTester("type_clash_tester", sen.api) tester.set_tests() + def update(): global tester tester.run_tests() diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..5b0e6e41 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,34 @@ +line-length = 120 +indent-width = 4 +include = ["*.py"] + +[format] +quote-style = "double" +indent-style = "space" +line-ending = "auto" + +[lint] +select = [ + "ARG", + "B", + "C", + "D", + "E", + "F", + "I", + "I", + "PLC", + "PLE", + "PLW", + "UP", + "W", +] +ignore = [ + "D105", # The meaning of special member functions can be found in the python documentation + "D203", # Conflicts with D211(blank-line-before-class) + "D212", # Conflicts with D213(multi-line-summary-second-line) +] +preview = true + +[lint.pydocstyle] +convention = "google" From bb61e4f194d31568a03537facb98a59f88196bad Mon Sep 17 00:00:00 2001 From: Luis Gutierrez Pereda Date: Tue, 26 May 2026 21:53:26 +0200 Subject: [PATCH 16/17] test(kernel): object synchronization integration tests First suit of integration tests that runs in a containerized manner. It tests object detection and update correctness between a set of local/remote participants. resolves SEN-1523 --- cmake/util/test.cmake | 2 +- cmake/util/tsan_ignorelist.txt | 16 + cmake/util/ubsan_ignorelist.txt | 2 + libs/kernel/src/bus/bus.cpp | 20 +- libs/kernel/src/kernel_impl.cpp | 2 +- libs/kernel/test/CMakeLists.txt | 5 + .../integration/object_sync/CMakeLists.txt | 433 +++++++++ .../object_sync/config/listener.yaml.in | 52 ++ .../object_sync/config/publisher.yaml.in | 26 + .../config/single_component.yaml.in | 29 + .../object_sync/config/single_process.yaml.in | 62 ++ .../test/integration/object_sync/readme.md | 77 ++ .../test/integration/object_sync/run.py | 155 ++++ .../object_sync/src/object_sync.cpp | 863 ++++++++++++++++++ .../object_sync/stl/object_sync.stl | 63 ++ 15 files changed, 1795 insertions(+), 12 deletions(-) create mode 100644 cmake/util/ubsan_ignorelist.txt create mode 100644 libs/kernel/test/integration/object_sync/CMakeLists.txt create mode 100644 libs/kernel/test/integration/object_sync/config/listener.yaml.in create mode 100644 libs/kernel/test/integration/object_sync/config/publisher.yaml.in create mode 100644 libs/kernel/test/integration/object_sync/config/single_component.yaml.in create mode 100644 libs/kernel/test/integration/object_sync/config/single_process.yaml.in create mode 100644 libs/kernel/test/integration/object_sync/readme.md create mode 100644 libs/kernel/test/integration/object_sync/run.py create mode 100644 libs/kernel/test/integration/object_sync/src/object_sync.cpp create mode 100644 libs/kernel/test/integration/object_sync/stl/object_sync.stl diff --git a/cmake/util/test.cmake b/cmake/util/test.cmake index 1af56b62..082d0bf9 100644 --- a/cmake/util/test.cmake +++ b/cmake/util/test.cmake @@ -160,7 +160,7 @@ endfunction() # [REQ_DEPS ] # ) function(add_sen_integration_test test_name) - set(_options) + set(_options FLAKY) set(_one_value_args) set(_multi_value_args REQ_COMPONENTS REQ_DEPS) diff --git a/cmake/util/tsan_ignorelist.txt b/cmake/util/tsan_ignorelist.txt index c9a81565..8af263ee 100644 --- a/cmake/util/tsan_ignorelist.txt +++ b/cmake/util/tsan_ignorelist.txt @@ -43,3 +43,19 @@ race:moodycamel::ConcurrentQueue msg) { Lock lock(remotesMutex_); - if (auto remotesItr = remotes_.find(to); remotesItr != remotes_.end()) - { - InputStream in(msg); - // read the header - uint8_t categoryVal = 0; - in.readUInt8(categoryVal); + InputStream in(msg); - sendMessageToParticipant(static_cast(categoryVal), in, *(*remotesItr).second); - } - else + // read the header + uint8_t categoryVal = 0; + in.readUInt8(categoryVal); + + if (const auto remotesItr = remotes_.find(to); remotesItr != remotes_.end()) { - logger_->debug("Bus {}.{}: remote message lost"); + sendMessageToParticipant(static_cast(categoryVal), in, *remotesItr->second); + return; } + + logger_->debug("Bus {}.{}: remote {} message lost.", address_.sessionName, address_.busName, categoryVal); } void Bus::remoteBroadcastMessageReceived(Span msg) diff --git a/libs/kernel/src/kernel_impl.cpp b/libs/kernel/src/kernel_impl.cpp index 3aa7a13f..1f91e5bd 100644 --- a/libs/kernel/src/kernel_impl.cpp +++ b/libs/kernel/src/kernel_impl.cpp @@ -279,7 +279,7 @@ void KernelImpl::sessionUnavailable(const std::string& name) const // NOSONAR std::shared_ptr KernelImpl::getKernelLogger() { - static std::shared_ptr logger = spdlog::stderr_color_mt("kernel"); + static std::shared_ptr logger = spdlog::stdout_color_mt("kernel"); return logger; } diff --git a/libs/kernel/test/CMakeLists.txt b/libs/kernel/test/CMakeLists.txt index f863b7e4..79b11ab3 100644 --- a/libs/kernel/test/CMakeLists.txt +++ b/libs/kernel/test/CMakeLists.txt @@ -14,4 +14,9 @@ if(NOT MSVC) add_subdirectory(integration/runtime_compatibility) add_subdirectory(integration/crash_report) add_subdirectory(integration/type_clash) + + # integration tests that use containers (only available in Linux) + if(LINUX) + add_subdirectory(integration/object_sync) + endif() endif() diff --git a/libs/kernel/test/integration/object_sync/CMakeLists.txt b/libs/kernel/test/integration/object_sync/CMakeLists.txt new file mode 100644 index 00000000..8990a8ff --- /dev/null +++ b/libs/kernel/test/integration/object_sync/CMakeLists.txt @@ -0,0 +1,433 @@ +# === CMakeLists.txt =================================================================================================== +# Sen Infrastructure +# Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +# See the LICENSE.txt file for more information. +# © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +# ====================================================================================================================== + +add_sen_package( + TARGET object_sync + STL_FILES stl/object_sync.stl + SOURCES src/object_sync.cpp + NO_SCHEMA + PRIVATE_DEPS spdlog::spdlog +) + +find_package(Python REQUIRED COMPONENTS Interpreter) + +function(generate_config_files DESTINATION) + file( + GLOB + templates + CONFIGURE_DEPENDS + config/*.in + ) + + set(template_names "") + foreach(full_path ${templates}) + get_filename_component(name_only ${full_path} NAME) + list(APPEND template_names ${name_only}) + endforeach() + + foreach(file ${template_names}) + string( + REPLACE ".in" + "" + output_file + ${file} + ) + configure_file(config/${file} ${DESTINATION}/${output_file} @ONLY) + endforeach() +endfunction() + +set(RYUK_IMAGE "docker-proxy.pforgeipt-docker.intra.airbusds.corp/testcontainers/ryuk:0.8.1") + +set(COMMON_TEST_ENV + "PYTHONUNBUFFERED=set:1;RYUK_CONTAINER_IMAGE=set:${RYUK_IMAGE};ASAN_OPTIONS=set:suppressions=/home/builder/sen/cmake/util/asan_ignorelist.txt:fast_unwind_on_malloc=0:malloc_context_size=100;TSAN_OPTIONS=set:suppressions=/home/builder/sen/cmake/util/tsan_ignorelist.txt;LSAN_OPTIONS=set:suppressions=/home/builder/sen/cmake/util/lsan_ignorelist.txt:report_objects=1;UBSAN_OPTIONS=set:suppressions=/home/builder/sen/cmake/util/ubsan_ignorelist.txt" +) + +set(_working_dir $) + +set(LISTENER_TYPE ListenerStaticProps) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/1_static_props) + +add_sen_integration_test( + object_sync_static_props_local_test + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/1_static_props/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_static_props_local_test ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_static_props_single_process_test + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/1_static_props/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_static_props_single_process_test ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_static_props_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/1_static_props/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/1_static_props/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_static_props_two_processes ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerBestEffortProps) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/2_best_effort_props) + +add_sen_integration_test( + object_sync_best_effort_props_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/2_best_effort_props/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_props_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_best_effort_props_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/2_best_effort_props/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_props_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_best_effort_props_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/2_best_effort_props/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/2_best_effort_props/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_props_two_processes ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerConfirmedProps) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/3_confirmed_props) + +add_sen_integration_test( + object_sync_confirmed_props_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/3_confirmed_props/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_props_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_confirmed_props_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/3_confirmed_props/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_props_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_confirmed_props_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/3_confirmed_props/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/3_confirmed_props/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_props_two_processes ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerWritableProps) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/4_writable_props) + +add_sen_integration_test( + object_sync_writable_props_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/4_writable_props/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_writable_props_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_writable_props_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/4_writable_props/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_writable_props_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_writable_props_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/4_writable_props/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/4_writable_props/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_writable_props_two_processes ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerBestEffortEvent) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/5_best_effort_events) + +add_sen_integration_test( + object_sync_best_effort_events_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/5_best_effort_events/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_events_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_best_effort_events_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/5_best_effort_events/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_events_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_best_effort_events_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/5_best_effort_events/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/5_best_effort_events/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_events_two_processes ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerConfirmedEvent) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/6_confirmed_events) + +add_sen_integration_test( + object_sync_confirmed_events_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/6_confirmed_events/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_events_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_confirmed_events_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/6_confirmed_events/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_events_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_confirmed_events_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/6_confirmed_events/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/6_confirmed_events/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_events_two_processes ${COMMON_TEST_ENV}) + +configure_file( + config/single_component.yaml.in ${CMAKE_CURRENT_BINARY_DIR}/7_local_method/single_component.yaml @ONLY +) + +add_sen_integration_test( + object_sync_local_method + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/7_local_method/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_local_method ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerConstMethod) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/8_const_method) + +add_sen_integration_test( + object_sync_const_method_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/8_const_method/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_const_method_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_const_method_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/8_const_method/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_const_method_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_const_method_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/8_const_method/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/8_const_method/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_const_method_two_processes ${COMMON_TEST_ENV}) diff --git a/libs/kernel/test/integration/object_sync/config/listener.yaml.in b/libs/kernel/test/integration/object_sync/config/listener.yaml.in new file mode 100644 index 00000000..a99268a0 --- /dev/null +++ b/libs/kernel/test/integration/object_sync/config/listener.yaml.in @@ -0,0 +1,52 @@ +kernel: + crashReportDisabled: true + +load: + - name: ether + group: 10 + discovery: + type: TcpDiscovery + value: + beamPeriod: 100 ms + beamExpiryTime: 1 s + hubAddress: + host: sen-hub + port: 65454 + +build: + - name: listenerComp1 + group: 1 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener1 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listenerComp2 + group: 1 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener2 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listenerComp3 + group: 1 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener3 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE name = "testObject" + bus: session.bus + - name: listenerComp4 + group: 1 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener4 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE staticProp = 15 + bus: session.bus diff --git a/libs/kernel/test/integration/object_sync/config/publisher.yaml.in b/libs/kernel/test/integration/object_sync/config/publisher.yaml.in new file mode 100644 index 00000000..afeccfab --- /dev/null +++ b/libs/kernel/test/integration/object_sync/config/publisher.yaml.in @@ -0,0 +1,26 @@ +kernel: + crashReportDisabled: true + +load: + - name: ether + group: 10 + runDiscoveryHub: 65454 + discovery: + type: TcpDiscovery + value: + beamPeriod: 100 ms + beamExpiryTime: 1 s + hubAddress: + host: sen-hub + port: 65454 + +build: + - name: myComponent + group: 1 + freqHz: 30 + imports: [object_sync] + objects: + - name: publisher + class: object_sync.PublisherImpl + numOfListeners: 4 + bus: session.bus diff --git a/libs/kernel/test/integration/object_sync/config/single_component.yaml.in b/libs/kernel/test/integration/object_sync/config/single_component.yaml.in new file mode 100644 index 00000000..73bf895c --- /dev/null +++ b/libs/kernel/test/integration/object_sync/config/single_component.yaml.in @@ -0,0 +1,29 @@ +kernel: + crashReportDisabled: true + +build: + - name: myComponent + group: 3 + freqHz: 50 + imports: [object_sync] + objects: + - name: publisher + class: object_sync.PublisherImpl + numOfListeners: 4 + bus: session.bus + - name: listener1 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listener2 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listener3 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE name = "testObject" + bus: session.bus + - name: listener4 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE staticProp = 15 + bus: session.bus diff --git a/libs/kernel/test/integration/object_sync/config/single_process.yaml.in b/libs/kernel/test/integration/object_sync/config/single_process.yaml.in new file mode 100644 index 00000000..ca14df89 --- /dev/null +++ b/libs/kernel/test/integration/object_sync/config/single_process.yaml.in @@ -0,0 +1,62 @@ +kernel: + crashReportDisabled: true + +load: + - name: ether + group: 10 + runDiscoveryHub: 65454 + discovery: + type: TcpDiscovery + value: + beamPeriod: 100 ms + beamExpiryTime: 1 s + hubAddress: + host: sen-hub + port: 65454 + +build: + - name: myComponent + group: 3 + freqHz: 30 + imports: [object_sync] + objects: + - name: publisher + class: object_sync.PublisherImpl + numOfListeners: 4 + bus: session.bus + - name: listenerComp1 + group: 3 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener1 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listenerComp2 + group: 3 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener2 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listenerComp3 + group: 3 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener3 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE name = "testObject" + bus: session.bus + - name: listenerComp4 + group: 3 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener4 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE staticProp = 15 + bus: session.bus diff --git a/libs/kernel/test/integration/object_sync/readme.md b/libs/kernel/test/integration/object_sync/readme.md new file mode 100644 index 00000000..5e4100ae --- /dev/null +++ b/libs/kernel/test/integration/object_sync/readme.md @@ -0,0 +1,77 @@ +# Object Synchronization integration tests + +These tests check data correctness in the communication between kernel participants in different +network configurations. + +The following objects are involved: + +- _publisher_: This object is published in the kernel pipeline configuration (yaml) and is in charge + of publishing the object under test, which will be detected by the listeners. It is used to + correctly sync with the listeners and avoid timing errors when running the listeners in a separate + component. + +- _testObject_: Contains all properties, events and methods whose correctness will be tested. It is + added and removed from the bus by the publisher. It contains a method called `doUpdate()`, which + will be called by the publisher when it certifies that the listeners have all detected the object. + This `doUpdate()` method enables automatic property updates and event emissions on every update of + the object (implemented in the `update()` method). + +- _listeners_: Collection of listener objects, four in the current version of the test, which + detect the test object and check whether the property updates, events or method responses received + are the expected ones. Each test checks a certain class member (e.g. confirmed property updates), + and in order to implement the specific checks for each test, various listener classes are derived + from a base `ListenerImpl` class. Additionally, each of the listeners have a configurable query + for the interest in the object, allowing to test different local participants with the same, or + different interests. + +The tests perform each check in three network configurations: + +- `Local`: The publisher, testObject and listeners all run in the same component. Local method + calling can be checked in this configuration. +- `Single process`: The publisher and the listeners run all in separated components, but all in the + same process. This asserts the correct functioning of the kernel with several local participants + in multiple threads (components). +- `Two processes`: The listeners are all in a different process, and also in different components + themselves, testing the detection of a remote object published by a remote participant by several + local participants in different threads. + +The implementation of the publisher and listeners contains logic that shuts down the kernels if the +checks pass. `run.py` just launches the processes, spits the logs through stdout, and exits with a +timeout (5 seconds) if processes do not exit automatically before. Any communication error between +the participants results in a timeout error in the integration test. If the error is in the +correctness of the data, assert statements will indicate precisely what failed. Each process is +executed in an isolated container spawned from `run.py` using the `test_containers` python library. + +The publisher and listeners interact in the following way (in a successful test): + +``` + ┌───────────┐ ┌─────────────┐ + │ Publisher │ │ Listeners │ + └─────┬─────┘ └──────┬──────┘ + │ │ + ▼ │ +(1) all listeners detected? │ + (add testObject) │ + │ ▼ + │ (2) test object detected? + │ (state -> ready) + ▼ │ + (3) all listeners ready? │ + (update testObject) │ + │ ▼ + │ (4) 10 updates received? + │ (state -> inSync) + ▼ │ + (5) all listeners inSync? │ + (remove testObject) │ + │ ▼ + │ (6) testObject removed? + │ (check correctness, state -> finished and kernel shutdown) + ▼ + (7) all listeners finished? + (kernel shutdown) + +``` + +As you can see from the diagram, both the listeners and publisher shutdown the kernel in case of a +correct execution. diff --git a/libs/kernel/test/integration/object_sync/run.py b/libs/kernel/test/integration/object_sync/run.py new file mode 100644 index 00000000..121da58e --- /dev/null +++ b/libs/kernel/test/integration/object_sync/run.py @@ -0,0 +1,155 @@ +# === run.py =========================================================================================================== +# Sen Infrastructure +# Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +# See the LICENSE.txt file for more information. +# © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +# ====================================================================================================================== + +# std +import os +import re +import sys +import time +import docker +import getpass +import subprocess +from pathlib import Path +from threading import Thread +from docker.models.containers import Container as WrappedContainer + +# testcontainers +from testcontainers.core.network import Network +from testcontainers.core.container import DockerContainer + +# constants +# TODO (SEN-1681) replace with a lighter runtime image +IMAGE_NAME = "sim-csr-docker.pforgeipt-docker.intra.airbusds.corp/sen-build-debian12:0.1.0-31-ge5dd492" +TIMEOUT = 5 + + +def stream_logs(cont: DockerContainer) -> None: + """formats and streams logs from the containers""" + w = cont.get_wrapped_container() + for line in w.logs(stream=True, follow=True): + if line: + print(f"[{w.short_id}] {line.decode('utf-8', 'replace').strip()}") + + +def is_container() -> bool: + """checks whether the script is running inside a container""" + try: + return os.path.exists("/.dockerenv") or any( + k in open("/proc/self/cgroup", "rt").read() for k in ("docker", "containerd") + ) + except (FileNotFoundError, PermissionError): + return False + + +def get_repo_root(start_path: Path) -> Path: + """returns the repo root path from a given path""" + for p in [start_path] + list(start_path.parents): + if (p / ".git").exists(): + return p + raise FileNotFoundError(f"Error: '{start_path}' is not inside a Git repo.") + + +def find_host_mount(container_path: Path) -> Path | None: + """finds the directory in the host where a certain container path is mounted""" + if not (mount_file := Path("/proc/self/mountinfo")).exists(): + raise FileNotFoundError("Could not access /proc/self/mountinfo. Host might not be a docker") + + ws = os.environ.get("WORKSPACE") + + mounts = list( + { + Path(ws if ws else parts[3]) + for line in open(mount_file, "r") + if str(container_path) in line and (parts := line.split()) + } + ) + + if len(mounts) > 1: + raise RuntimeError(f"Error: Multiple Sen repository mounts detected! {mounts}.") + return mounts[0] if mounts else (print("No Sen repository mounts found.") or None) + + +def check_image_availability(image: str) -> None: + """reports an error if the container image is not found in the docker cache""" + try: + client = docker.from_env() + client.images.get(image) + except docker.errors.ImageNotFound: + raise RuntimeError("Container Image not found in the local cache. Please pull the image first.") + except docker.errors.DockerException as e: + raise RuntimeError(f"Could not connect to the local Docker daemon: {e}") + + +def abort(container_list: list[DockerContainer], thread_list: list[Thread]) -> None: + """stops containers and joins log threads before aborting with error""" + list(map(lambda elem: elem.stop(), container_list)) + list(map(Thread.join, [t for t in thread_list if isinstance(t, Thread)])) + sys.exit(1) + + +if __name__ == "__main__": + if len(sys.argv) < 3: + sys.exit("Usage: python run.py ...") + + cmake_workdir = sys.argv[1] + configs = sys.argv[2:] + + check_image_availability(IMAGE_NAME) + + # repo root dir in the environment used in cmake (it can be the host machine or a container) + cmake_repo_root = get_repo_root(Path(__file__).parent) + + # repo root dir in the host machine (if cmake is being configured in a container) + host_repo_root = find_host_mount(cmake_repo_root) if is_container() else cmake_repo_root + + # repo root dir in the container used to execute the integration tests + test_repo_root = Path("/home/builder/sen") + test_workdir = test_repo_root / Path(cmake_workdir).relative_to(cmake_repo_root) + + # container paths + with Network() as network: + containers = [] + log_threads = [] + + for i, config in enumerate(configs): + aliases = ["sen-hub"] if i == 0 else [] + + # container definition + container = ( + DockerContainer(IMAGE_NAME) + .with_network(network) + .with_network_aliases(*aliases) + .with_volume_mapping(host_repo_root, "/home/builder/sen", mode="rw") + .with_command(f"./cli_run {test_repo_root / Path(config).relative_to(cmake_repo_root)}") + .with_kwargs(working_dir=str(test_workdir), cap_add=["SYS_ADMIN"], security_opt=["seccomp=unconfined"]) + ) + + # start the container (with the host environment) and the log thread + container.env.update(os.environ) + container.start() + log_threads.append(Thread(target=stream_logs, args=(container,), daemon=True).start()) + containers.append(container) + + deadline = time.time() + TIMEOUT + while time.time() < deadline: + wrapped = [c.get_wrapped_container() for c in containers] + list(map(lambda w: w.reload(), wrapped)) + + # abort if any of the processes has exited with error + if any(w.status == "exited" and w.wait()["StatusCode"] != 0 for w in wrapped): + abort(containers, log_threads) + + # pass the test if all processes have exited successfully + if all(w.status == "exited" for w in wrapped): + # join the logger threads + list(map(Thread.join, [t for t in log_threads if isinstance(t, Thread)])) + break + + time.sleep(0.2) + else: + print(f"\nError: timeout of {TIMEOUT}s reached!") + abort(containers, log_threads) diff --git a/libs/kernel/test/integration/object_sync/src/object_sync.cpp b/libs/kernel/test/integration/object_sync/src/object_sync.cpp new file mode 100644 index 00000000..88b353b0 --- /dev/null +++ b/libs/kernel/test/integration/object_sync/src/object_sync.cpp @@ -0,0 +1,863 @@ +// === object_sync.cpp ================================================================================================= +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +#include "stl/object_sync.stl.h" + +// sen +#include "sen/core/base/assert.h" +#include "sen/core/base/compiler_macros.h" +#include "sen/core/base/numbers.h" +#include "sen/core/meta/class_type.h" +#include "sen/core/meta/var.h" +#include "sen/core/obj/connection_guard.h" +#include "sen/core/obj/interest.h" +#include "sen/core/obj/object.h" +#include "sen/core/obj/object_list.h" +#include "sen/core/obj/object_source.h" +#include "sen/core/obj/subscription.h" +#include "sen/kernel/component_api.h" + +// spdlog +#include +#include + +// std +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace object_sync +{ + +namespace +{ + +constexpr std::size_t generatorSeed = 2155648U; +constexpr uint32_t numOfChecks = 10U; +constexpr uint8_t staticPropValue = 15U; +constexpr auto staticNoConfigPropValue = TestEnum::second; + +[[nodiscard]] std::string generateString(std::mt19937& gen, const int length = 10) +{ + static constexpr std::string_view charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + std::string result; + result.reserve(length); + std::uniform_int_distribution dist(0, charset.size() - 1); + + std::generate_n(std::back_inserter(result), length, [&]() { return charset[dist(gen)]; }); + + return result; +} + +[[nodiscard]] TestStruct generateStruct(std::mt19937& gen) +{ + std::uniform_int_distribution distInteger; + std::uniform_real_distribution distFloat(0.0f, 1000.0f); + std::uniform_int_distribution distStrLen(1U, 6U); + + return {distInteger(gen), generateString(gen, static_cast(distStrLen(gen))), distFloat(gen)}; +} + +} // namespace + +/// Object used when testing class member synchronization +class TestObjectImpl final: public TestObjectBase +{ +public: + SEN_NOCOPY_NOMOVE(TestObjectImpl) + +public: + TestObjectImpl(const std::string& name, const u8 staticProp) + : TestObjectBase(name, staticProp) + , logger_(spdlog::stdout_color_mt(name)) + , gen_(generatorSeed) + , localMethodGen_(generatorSeed) + { + // set static no config property + setNextStaticNoConfigProp(staticNoConfigPropValue); + } + + ~TestObjectImpl() override = default; + +public: + void update(sen::kernel::RunApi& runApi) override + { + std::ignore = runApi; + + if (!doUpdate_) + { + return; + } + + // keeps track of the number of updates + setNextUpdateId(++updateCounter_); + + // update best effort prop + gen_.seed(generatorSeed); + gen_.discard(updateCounter_); + setNextBestEffortProp(std::uniform_real_distribution()(gen_)); + + // update confirmed prop + gen_.seed(generatorSeed); + gen_.discard(updateCounter_); + setNextConfirmedProp(generateStruct(gen_)); + + // best effort event + gen_.seed(generatorSeed); + gen_.discard(updateCounter_); + bestEffortEvent(updateCounter_, generateStruct(gen_)); + + // confirmed event + gen_.seed(generatorSeed); + gen_.discard(updateCounter_); + confirmedEvent(updateCounter_, generateString(gen_)); + } + +protected: + [[nodiscard]] u32 constMethodImpl(const TestEnum& arg) const override { return static_cast(arg); } + [[nodiscard]] u8 confirmedMethodImpl(u64 arg) override { return static_cast(arg); } + [[nodiscard]] bool bestEffortMethodImpl(const std::string& arg) override + { + std::ignore = arg; + return true; + } + [[nodiscard]] u16 localMethod() override { return localMethodDist_(gen_); } + void doUpdateImpl() override { doUpdate_ = true; } + +private: + std::shared_ptr logger_; + uint32_t updateCounter_ = 0U; + std::mt19937 gen_; + bool doUpdate_ = false; + + // method distributions + std::mt19937 localMethodGen_; + std::uniform_int_distribution localMethodDist_; +}; + +/// Publishes/Unpublishes the TestObject +class PublisherImpl final: public PublisherBase +{ +public: + SEN_NOCOPY_NOMOVE(PublisherImpl) + +public: + PublisherImpl(std::string name, const sen::VarMap& args) + : PublisherBase(name, args), logger_(spdlog::stdout_color_mt(name)) + { + } + + ~PublisherImpl() override = default; + +public: + void registered(sen::kernel::RegistrationApi& api) override + { + listenerStates_.reserve(getNumOfListeners()); + + // publish the test object once all listeners have been detected + guards_.emplace_back(onListenersReadyChanged({this, + [this]() + { + // publish the test object + object_->doUpdate(); + }})); + + // detect listeners (used for test shutdown) + listenerSub_ = api.selectAllFrom( + "session.bus", + [this, &api](const auto& addedObjects) + { + detectedListeners_ += std::distance(addedObjects.begin(), addedObjects.end()); + for (auto* listener: addedObjects) + { + listenerStates_[listener->asObject().getId()] = listener->getState(); + guards_.emplace_back( + listener->onStateChanged({this, + [this, &api, listener]() + { + std::ignore = api; + const auto& id = listener->asObject().getId(); + const auto& name = listener->asObject().getName(); + const auto& state = listener->getState(); + + logger_->info("{} received on state changed from {} to {}", + getName(), + name, + sen::StringConversionTraits::toString(state)); + + listenerStates_[id] = state; + + // remove the test object from the bus if all listeners are in sync + if (allListenersWithState(ListenerState::inSync)) + { + listenerStates_.clear(); + + logger_->info("{} removing {}", getName(), object_->getName()); + bus_->remove(object_); + } + + // shutdown the process kernel if all listeners are finished + if (allListenersWithState(ListenerState::finished)) + { + logger_->info("{} commanding kernel stop", getName()); + api.requestKernelStop(); + } + + if (allListenersWithState(ListenerState::ready)) + { + setNextListenersReady(true); + logger_->info("listeners ready"); + } + }})); + } + + // publish the test object when all expected listeners have been detected + if (detectedListeners_ == getNumOfListeners()) + { + // publish the test object + bus_ = api.getSource("session.bus"); + object_ = std::make_shared("testObject", staticPropValue); + logger_->info("publishing test object"); + bus_->add(object_); + } + }); + } + +private: + [[nodiscard]] bool allListenersWithState(const ListenerState state) + { + return listenerStates_.size() == getNumOfListeners() && + std::all_of(listenerStates_.begin(), + listenerStates_.end(), + [state](const auto& pair) { return pair.second == state; }); + } + +private: + std::shared_ptr logger_; + std::shared_ptr bus_; + std::shared_ptr object_; + std::shared_ptr> listenerSub_; + std::unordered_map listenerStates_; + std::vector guards_; + uint32_t detectedListeners_ = 0U; +}; + +SEN_EXPORT_CLASS(PublisherImpl) + +/// Contains all data from TestObject received on the listeners +struct ListenedData +{ + // static props + uint8_t staticProp; + TestEnum staticNoConfigProp; + + // dynamic props + std::vector bestEffortPropUpdates; + std::vector confirmedPropUpdates; + std::vector writablePropUpdates; + + // events + std::unordered_map bestEffortEventData; + std::unordered_map confirmedEventData; +}; + +/// Detects changes in the TestObject members (from the same, or a different component or process) +class ListenerImpl: public ListenerBase +{ +public: + SEN_NOCOPY_NOMOVE(ListenerImpl) + +public: + ListenerImpl(std::string name, const sen::VarMap& args) + : ListenerBase(name, args), logger_(spdlog::stdout_color_mt(name)) + { + } + + ~ListenerImpl() override = default; + +public: + void registered(sen::kernel::RegistrationApi& api) override + { + // detect all other listeners + listenerSub_ = api.selectAllFrom("session.bus"); + + // listen to test objects + bus_ = api.getSource("session.bus"); + bus_->addSubscriber(sen::Interest::make(getQuery(), api.getTypes()), &objList_, true); + + std::ignore = objList_.onAdded( + [this](const auto& addedObjects) + { + if (addedObjects.begin() != addedObjects.end()) + { + testObject_ = *addedObjects.begin(); + + logger_->info("{} detected {}", getName(), testObject_->asObject().getName()); + + // store static data + data_.staticProp = testObject_->getStaticProp(); + data_.staticNoConfigProp = testObject_->getStaticNoConfigProp(); + doSync(); + + // property update callbacks + objGuards_.emplace_back(testObject_->onBestEffortPropChanged( + {this, + [this]() + { + doSync(testObject_->getUpdateId()); + + if (data_.bestEffortPropUpdates.size() < numOfChecks) + { + data_.bestEffortPropUpdates.push_back(testObject_->getBestEffortProp()); + } + }})); + objGuards_.emplace_back(testObject_->onConfirmedPropChanged( + {this, + [this]() + { + doSync(testObject_->getUpdateId()); + if (data_.confirmedPropUpdates.size() < numOfChecks) + { + data_.confirmedPropUpdates.push_back(testObject_->getConfirmedProp()); + } + }})); + objGuards_.emplace_back( + testObject_->onWritablePropChanged({this, + [this]() + { + doSync(testObject_->getUpdateId()); + if (data_.writablePropUpdates.size() < numOfChecks) + { + data_.writablePropUpdates.push_back(testObject_->getWritableProp()); + } + }})); + + objGuards_.emplace_back( + testObject_->onBestEffortEvent({this, + [this](const uint32_t id, const TestStruct& arg) + { + if (data_.bestEffortEventData.size() < numOfChecks) + { + if (!data_.bestEffortEventData.insert({id, arg}).second) + { + // Key was repeated; result.first points to the existing + // element + repeatedEventsReceived_ = true; + } + } + }})); + objGuards_.emplace_back( + testObject_->onConfirmedEvent({this, + [this](const uint32_t id, const std::string& arg) + { + if (data_.confirmedEventData.size() < numOfChecks) + { + if (!data_.confirmedEventData.insert({id, arg}).second) + { + // Key was repeated; result.first points to the existing + // element + repeatedEventsReceived_ = true; + } + } + }})); + + setNextState(ListenerState::ready); + } + }); + + std::ignore = objList_.onRemoved( + [this](const auto& removedObjects) + { + if (removedObjects.begin() != removedObjects.end()) + { + // clear callbacks associated to the object + testObject_ = nullptr; + doChecks(); + setNextState(ListenerState::finished); + logger_->info("{} moved to finished state", getName()); + } + }); + + // detect when the listener has finished and stop the kernel if all other listeners are finished + listenersGuard_ = + onStateChanged({this, + [this, &api]() + { + // if all listeners are finished, stop the process + if (std::all_of(listenerSub_->list.getObjects().begin(), + listenerSub_->list.getObjects().end(), + [](const auto* elem) { return elem->getState() == ListenerState::finished; })) + { + logger_->info("stopping listener process", getName()); + api.requestKernelStop(); + } + }}); + } + +protected: + std::shared_ptr& getLogger() { return logger_; } + [[nodiscard]] const ListenedData& getData() const noexcept { return data_; } + [[nodiscard]] TestObjectInterface* getTestObject() const noexcept { return testObject_; } + [[nodiscard]] uint32_t getFirstUpdateId() const noexcept { return firstUpdateId_; } + [[nodiscard]] bool getRepeatedEventsReceived() const noexcept { return repeatedEventsReceived_; } + +protected: + /// Synchronizes received updates and change the state to onSync when the required updates are received + virtual void doSync(uint32_t updateId = 0) // NOLINT [google-default-arguments] + { + if (firstUpdateId_ == 0) + { + firstUpdateId_ = updateId; + } + } + +private: + /// Asserts the correctness of the property updates received + virtual void doChecks() {} + +private: + std::shared_ptr bus_; + sen::ObjectList objList_; + TestObjectInterface* testObject_ = nullptr; + std::shared_ptr> listenerSub_; + std::vector objGuards_; + sen::ConnectionGuard listenersGuard_; + std::shared_ptr logger_; + ListenedData data_ {}; + uint32_t firstUpdateId_ = 0; // first update ID received by the listener + bool repeatedEventsReceived_ = + false; // true if the events where received more than once in each participant (previous bug) + std::vector listeners_; +}; + +/// Listener that checks if static props are synchronized correctly +class ListenerStaticProps final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerStaticProps) + +public: + using ListenerImpl::ListenerImpl; + ~ListenerStaticProps() override = default; + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + std::ignore = updateId; + + if (getData().staticProp != 0U) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& data = getData(); + + SEN_ASSERT(data.staticProp == staticPropValue); + SEN_ASSERT(data.staticNoConfigProp == staticNoConfigPropValue); + } +}; + +SEN_EXPORT_CLASS(ListenerStaticProps) + +/// Listener that checks if static props are synchronized correctly +class ListenerBestEffortProps final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerBestEffortProps) + +public: + using ListenerImpl::ListenerImpl; + ~ListenerBestEffortProps() override = default; + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + ListenerImpl::doSync(updateId); + + if (getData().bestEffortPropUpdates.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& data = getData(); + + SEN_ASSERT(data.bestEffortPropUpdates.size() == numOfChecks); + + // generator for the updates + for (uint32_t i = 0; i < numOfChecks; ++i) + { + gen_.seed(generatorSeed); + gen_.discard(getFirstUpdateId() + i); + SEN_ASSERT(data.bestEffortPropUpdates[i] - std::uniform_real_distribution()(gen_) < 1e-6); + } + } + +private: + std::mt19937 gen_ {generatorSeed}; +}; + +SEN_EXPORT_CLASS(ListenerBestEffortProps) + +/// Listener that checks if static props are synchronized correctly +class ListenerConfirmedProps final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerConfirmedProps) + +public: + using ListenerImpl::ListenerImpl; + ~ListenerConfirmedProps() override = default; + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + ListenerImpl::doSync(updateId); + + if (getData().confirmedPropUpdates.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& data = getData(); + SEN_ASSERT(data.confirmedPropUpdates.size() == numOfChecks); + + // generator for the updates + for (uint32_t i = 0; i < numOfChecks; ++i) + { + std::mt19937 gen(generatorSeed); + gen.discard(getFirstUpdateId() + i); + SEN_ASSERT(data.confirmedPropUpdates[i] == generateStruct(gen)); + } + } +}; + +SEN_EXPORT_CLASS(ListenerConfirmedProps) + +/// Listener that checks if writable props are synchronized . We just send the update ID in the writable prop directly +class ListenerWritableProps final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerWritableProps) + +public: + ListenerWritableProps(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed) + { + } + + ~ListenerWritableProps() override = default; + +public: + void update(sen::kernel::RunApi& runApi) override + { + std::ignore = runApi; + + // set the writable property + if (auto* obj = getTestObject(); obj != nullptr) + { + obj->setNextWritableProp({counter_++, updateDist_(gen_)}); + } + } + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + std::ignore = updateId; + + if (getData().writablePropUpdates.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& updates = getData().writablePropUpdates; + + for (const auto& [id, value]: updates) + { + gen_.seed(generatorSeed); + updateDist_.reset(); + gen_.discard(id); + SEN_ASSERT(value == updateDist_(gen_)); + } + } + +private: + uint32_t counter_ = 0U; + std::mt19937_64 gen_; + std::uniform_int_distribution updateDist_; +}; + +SEN_EXPORT_CLASS(ListenerWritableProps) + +/// Listener that checks if best effort events are transmitted correctly +class ListenerBestEffortEvent final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerBestEffortEvent) + +public: + ListenerBestEffortEvent(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed) + { + } + + ~ListenerBestEffortEvent() override = default; + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + std::ignore = updateId; + if (getData().bestEffortEventData.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& data = getData(); + SEN_ASSERT(data.bestEffortEventData.size() == numOfChecks); + + // generator for the updates + for (const auto& [id, value]: data.bestEffortEventData) + { + gen_.seed(generatorSeed); + gen_.discard(id); + SEN_ASSERT(value == generateStruct(gen_)); + } + // check that the event was not received multiple times + SEN_ASSERT(!getRepeatedEventsReceived()); + } + +private: + std::mt19937 gen_; +}; + +SEN_EXPORT_CLASS(ListenerBestEffortEvent) + +/// Listener that checks if confirmed events are transmitted correctly +class ListenerConfirmedEvent final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerConfirmedEvent) + +public: + ListenerConfirmedEvent(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed) + { + } + + ~ListenerConfirmedEvent() override = default; + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + std::ignore = updateId; + if (getData().confirmedEventData.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& data = getData(); + SEN_ASSERT(data.confirmedEventData.size() == numOfChecks); + + // generator for the updates + for (const auto& [id, value]: data.confirmedEventData) + { + gen_.seed(generatorSeed); + gen_.discard(id); + SEN_ASSERT(value == generateString(gen_)); + } + + // check that the event was not received multiple times + SEN_ASSERT(!getRepeatedEventsReceived()); + } + +private: + std::mt19937 gen_; +}; + +SEN_EXPORT_CLASS(ListenerConfirmedEvent) + +/// Listener that checks if confirmed events are transmitted correctly +class ListenerLocalMethod final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerLocalMethod) + +public: + ListenerLocalMethod(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed) + { + returnValues_.reserve(numOfChecks); + } + ~ListenerLocalMethod() override = default; + +public: + void update(sen::kernel::RunApi& runApi) override + { + std::ignore = runApi; + if (auto* testObject = getTestObject(); testObject != nullptr) + { + returnValues_.push_back(testObject->localMethod()); + + if (returnValues_.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + } + +private: // implements ListenerImpl + void doChecks() override + { + // generator for the updates + for (const auto value: returnValues_) + { + SEN_ASSERT(value == distribution_(gen_)); + } + } + +private: + std::mt19937 gen_; + std::uniform_int_distribution distribution_; + std::vector returnValues_; +}; + +SEN_EXPORT_CLASS(ListenerLocalMethod) + +/// Listener that checks if confirmed return correctly when called +class ListenerConstMethod final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerConstMethod) + +public: + ListenerConstMethod(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed), distribution_(0U, 2U) + { + returnValues_.reserve(numOfChecks); + } + + ~ListenerConstMethod() override = default; + +public: + void update(sen::kernel::RunApi& runApi) override + { + std::ignore = runApi; + if (const auto* testObject = getTestObject(); testObject != nullptr) + { + testObject->constMethod(static_cast(distribution_(gen_)), + {this, + [this](const auto& response) + { + if (response) + { + returnValues_.push_back(response.getValue()); + + if (returnValues_.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + }}); + } + } + +private: // implements ListenerImpl + void doChecks() override + { + // check that we have collected responses within range (expected values) + SEN_ASSERT(std::all_of(returnValues_.begin(), returnValues_.end(), [](uint32_t val) { return val <= 2U; })); + } + +private: + std::mt19937 gen_; + std::uniform_int_distribution distribution_; + std::vector returnValues_; +}; + +SEN_EXPORT_CLASS(ListenerConstMethod) + +/// Listener that checks if confirmed methods return correctly when called +class ListenerConfirmedMethod final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerConfirmedMethod) + +public: + ListenerConfirmedMethod(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed), distribution_(0U, 2U) + { + returnValues_.reserve(numOfChecks); + } + + ~ListenerConfirmedMethod() override = default; + +public: + void update(sen::kernel::RunApi& runApi) override + { + std::ignore = runApi; + if (const auto* testObject = getTestObject(); testObject != nullptr) + { + testObject->constMethod(static_cast(distribution_(gen_)), + {this, + [this](const auto& response) + { + if (response) + { + returnValues_.push_back(response.getValue()); + + if (returnValues_.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + }}); + } + } + +private: // implements ListenerImpl + void doChecks() override + { + // check that we have collected responses within range (expected values) + SEN_ASSERT(std::all_of(returnValues_.begin(), returnValues_.end(), [](uint32_t val) { return val <= 2U; })); + } + +private: + std::mt19937 gen_; + std::uniform_int_distribution distribution_; + std::vector returnValues_; +}; + +SEN_EXPORT_CLASS(ListenerConfirmedMethod) + +} // namespace object_sync diff --git a/libs/kernel/test/integration/object_sync/stl/object_sync.stl b/libs/kernel/test/integration/object_sync/stl/object_sync.stl new file mode 100644 index 00000000..eb63cec7 --- /dev/null +++ b/libs/kernel/test/integration/object_sync/stl/object_sync.stl @@ -0,0 +1,63 @@ +package object_sync; + +enum ListenerState : u8 +{ + connecting, + ready, + inSync, + finished +} + +class Publisher +{ + var numOfListeners : u32 [static]; + var listenersReady : bool; +} + +class Listener +{ + var query : string [static, confirmed]; + var state : ListenerState [confirmed]; +} + +struct TestStruct +{ + field1 : u32, + field2 : string, + field3 : f32 +} + +enum TestEnum : u8 +{ + first, + second, + third +} + +// Type used to evaluate correctness of writable properties through the network. Id is incremented and it is used to generate the value (which is then checked when reading ) +struct WritablePropType +{ + id : u32, + value : u64 +} + +class TestObject +{ + var staticProp : u8 [static]; + var staticNoConfigProp : TestEnum [static_no_config]; + var updateId : u32 [confirmed]; // used to verify the synchronization of dynamic properties + var bestEffortProp : f64 [bestEffort]; + var confirmedProp : TestStruct [confirmed]; + var writableProp : WritablePropType [writable]; + + event bestEffortEvent(id: u32, arg: TestStruct) [bestEffort]; + event confirmedEvent(id: u32, arg: string) [confirmed]; + + fn constMethod(arg: TestEnum) -> u32 [const]; + fn confirmedMethod(arg: u64) -> u8 [confirmed]; + fn bestEffortMethod(arg: string) -> bool [bestEffort]; + fn localMethod() -> u16 [local]; + + // method to command the object updates to start + fn doUpdate() [confirmed]; +} From 0d15e90ee20a0935cff1acabcd4cad81ab790fac Mon Sep 17 00:00:00 2001 From: Florian Sattler Date: Tue, 26 May 2026 22:10:21 +0200 Subject: [PATCH 17/17] chore: cleans up ruff linter warnings across the codebase resolves SEN-1688 --- .cmake-format.py | 13 +-- .conan/test_packages/package/conanfile.py | 11 ++- .github/scripts/generate_matrix_jobs.py | 94 +++++++++++-------- .pre-commit-config.yaml | 6 +- ruff.toml => .ruff.toml | 0 README.md | 2 +- apps/cli_gen/test/test19/test.py | 28 ++++-- apps/cli_gen/test/test20/test.py | 26 +++-- cmake/util/color_cmake_target_graph.py | 53 +++++++---- cmake/util/generate_coverage_report.py | 13 ++- components/py/test/src/modify_objects.py | 25 ++--- .../recorder/test/data/gen_crash_record.py | 13 ++- conanfile.py | 52 ++++++++-- examples/apps/rest_python/sen_client.py | 47 +++++----- .../10_python/scripts/creating_objects.py | 10 +- .../config/10_python/scripts/hello_python.py | 8 +- .../10_python/scripts/inspecting_objects.py | 8 +- .../scripts/interacting_with_objects.py | 6 +- .../scripts/reacting_to_events_and_changes.py | 11 ++- .../6_recorder/3_recorder_school_print.py | 4 +- .../crash_report/crash_report_tester.py | 8 +- .../test/integration/object_sync/run.py | 44 +++++---- libs/kernel/test/integration/runner.py | 17 +++- libs/kernel/test/integration/tester.py | 84 ++++++++--------- .../type_clash/type_clash_tester.py | 18 ++-- 25 files changed, 367 insertions(+), 234 deletions(-) rename ruff.toml => .ruff.toml (100%) diff --git a/.cmake-format.py b/.cmake-format.py index 93b4fed0..2bef5bc6 100644 --- a/.cmake-format.py +++ b/.cmake-format.py @@ -4,11 +4,12 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Config file that specifies how cmake-format should behave.""" # ---------------------------------- # Options affecting listfile parsing # ---------------------------------- -with section("parse"): +with section("parse"): # noqa: F821 # Specify structure for custom cmake functions additional_commands = { "add_sen_integration_test": { @@ -173,7 +174,7 @@ # ----------------------------- # Options affecting formatting. # ----------------------------- -with section("format"): +with section("format"): # noqa: F821 # Disable formatting entirely, making cmake-format a no-op disable = False @@ -270,7 +271,7 @@ # ------------------------------------------------ # Options affecting comment reflow and formatting. # ------------------------------------------------ -with section("markup"): +with section("markup"): # noqa: F821 # What character to use for bulleted lists bullet_char = "-" @@ -313,7 +314,7 @@ # ---------------------------- # Options affecting the linter # ---------------------------- -with section("lint"): +with section("lint"): # noqa: F821 # a list of lint codes to disable disabled_codes = [] @@ -369,7 +370,7 @@ # ------------------------------- # Options affecting file encoding # ------------------------------- -with section("encode"): +with section("encode"): # noqa: F821 # If true, emit the unicode byte-order mark (BOM) at the start of the file emit_byteorder_mark = False @@ -383,7 +384,7 @@ # ------------------------------------- # Miscellaneous configurations options. # ------------------------------------- -with section("misc"): +with section("misc"): # noqa: F821 # A dictionary containing any per-command configuration overrides. Currently # only `command_case` is supported. per_command = {} diff --git a/.conan/test_packages/package/conanfile.py b/.conan/test_packages/package/conanfile.py index 28e55ca1..4f65febe 100644 --- a/.conan/test_packages/package/conanfile.py +++ b/.conan/test_packages/package/conanfile.py @@ -4,32 +4,37 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== - -# test to consume conan package in conan v2 way +"""Module that defines a test_package test to consume conan package in conan v2 way.""" from conan import ConanFile -from conan.tools.cmake import CMakeToolchain, CMake from conan.tools.build import cross_building +from conan.tools.cmake import CMake, CMakeToolchain class TestPackageConan(ConanFile): + """Conan file that specifies how a project is setup that uses Sen.""" + settings = "os", "arch", "compiler", "build_type" generators = "CMakeDeps", "VirtualRunEnv" test_type = "explicit" def requirements(self): + """Defines the dependencies of Sen.""" self.requires(self.tested_reference_str) def generate(self): + """Generate the toolchain files.""" tc = CMakeToolchain(self, generator="Ninja") tc.generate() def build(self): + """Configure and build Sen test_package.""" cmake = CMake(self) cmake.configure() cmake.build() def test(self): + """Defines a few test calls to ensure Sen works.""" if not cross_building(self): self.run("sen --version", env="conanrun") # TODO(SEN-1185): enable tests when possible diff --git a/.github/scripts/generate_matrix_jobs.py b/.github/scripts/generate_matrix_jobs.py index 0fb680ba..77169b08 100644 --- a/.github/scripts/generate_matrix_jobs.py +++ b/.github/scripts/generate_matrix_jobs.py @@ -4,30 +4,27 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== -""" -Script to generate various forms of job matrices. -""" -import os +"""Script to generate various forms of job matrices.""" + +import argparse import json +import os import typing as tp -import argparse -from dataclasses import dataclass, is_dataclass, asdict +from dataclasses import asdict, dataclass, is_dataclass def convert_dataclass_to_json(obj) -> dict: """Converts the given dataclass to json.""" if is_dataclass(obj): - return { - key: convert_dataclass_to_json(value) - for key, value in asdict(obj).items() - } + return {key: convert_dataclass_to_json(value) for key, value in asdict(obj).items()} return obj @dataclass(frozen=True, order=True) class Compiler: - """Compiler specification""" + """Compiler specification.""" + name: str version: int cc: str @@ -36,13 +33,15 @@ class Compiler: @dataclass(frozen=True, order=True) class Container: - """Container specification""" + """Container specification.""" + image: str @dataclass(frozen=True, order=True) class JobSpecification: """Pipeline job specification that defines the configuration options.""" + name: str os: str runner: tp.Literal["ubuntu-latest", "windows-2022", "self-hosted", "ubuntu-24.04-arm"] @@ -56,47 +55,65 @@ def as_json(self) -> dict: return convert_dataclass_to_json(self) -def compute_jobs(release: bool, conan: bool) -> list[JobSpecification]: +def compute_jobs(release: bool, conan: bool) -> list[JobSpecification]: # noqa: ARG001 """Computes the list of pipeline jobs that should run.""" jobs = [] # Add gcc jobs if not release: jobs.append( - JobSpecification("Basic GCC", "ubuntu-22.04", "self-hosted", None, - Compiler("gcc", 12, "gcc-12", "g++-12"), 17, - "Debug")) + JobSpecification( + "Basic GCC", "ubuntu-22.04", "self-hosted", None, Compiler("gcc", 12, "gcc-12", "g++-12"), 17, "Debug" + ) + ) else: jobs.append( - JobSpecification("Basic GCC", "ubuntu-22.04", "self-hosted", None, - Compiler("gcc", 12, "gcc-12", "g++-12"), 17, - "Release")) + JobSpecification( + "Basic GCC", "ubuntu-22.04", "self-hosted", None, Compiler("gcc", 12, "gcc-12", "g++-12"), 17, "Release" + ) + ) # Add clang jobs if not release: jobs.append( - JobSpecification("Basic Clang", "ubuntu-24.04", "self-hosted", - None, - Compiler("clang", 20, "clang-20", - "clang++-20"), 17, "Debug")) + JobSpecification( + "Basic Clang", + "ubuntu-24.04", + "self-hosted", + None, + Compiler("clang", 20, "clang-20", "clang++-20"), + 17, + "Debug", + ) + ) # Add msvc jobs if release: jobs.append( - JobSpecification("Basic Windows", "windows", "windows-2022", None, - Compiler("msvc", 194, "cl", "cl"), 17, "Release")) + JobSpecification( + "Basic Windows", "windows", "windows-2022", None, Compiler("msvc", 194, "cl", "cl"), 17, "Release" + ) + ) else: jobs.append( - JobSpecification("Basic Windows", "windows", "windows-2022", None, - Compiler("msvc", 194, "cl", "cl"), 17, "Debug")) + JobSpecification( + "Basic Windows", "windows", "windows-2022", None, Compiler("msvc", 194, "cl", "cl"), 17, "Debug" + ) + ) # Add amd64 jobs if not release: jobs.append( - JobSpecification("Basic Ubuntu arm", "ubuntu-24.04", - "ubuntu-24.04-arm", None, - Compiler("gcc_arm64", 12, "gcc-14", - "g++-14"), 17, "Debug")) + JobSpecification( + "Basic Ubuntu arm", + "ubuntu-24.04", + "ubuntu-24.04-arm", + None, + Compiler("gcc_arm64", 12, "gcc-14", "g++-14"), + 17, + "Debug", + ) + ) return sorted(jobs) @@ -106,9 +123,8 @@ def generate_jobs_file(release: bool, conan: bool) -> None: jobs = compute_jobs(release, conan) if output_file := os.environ.get("GITHUB_OUTPUT"): - with open(output_file, "wt", encoding='utf-8') as matrix_file: - matrix_file.write( - f"jobs={json.dumps([j.as_json() for j in jobs])}") + with open(output_file, "w", encoding="utf-8") as matrix_file: + matrix_file.write(f"jobs={json.dumps([j.as_json() for j in jobs])}") else: print("Error: No output file specified to write to.") @@ -117,18 +133,18 @@ def main() -> None: """Runs the job matrix generator.""" parser = argparse.ArgumentParser( prog="generate_matrix_jobs", - description= - "Generates the list of required matrix jobs for various building needs." + description="Generates the list of required matrix jobs for various building needs.", ) parser.add_argument( "--release", action=argparse.BooleanOptionalAction, - help="Generate the jobs needed for building release artifacts.") + help="Generate the jobs needed for building release artifacts.", + ) parser.add_argument( "--conan", action=argparse.BooleanOptionalAction, - help= - "Generate the jobs needed to ensure all required conan packages work.") + help="Generate the jobs needed to ensure all required conan packages work.", + ) args = parser.parse_args() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25b28e1d..5ebaf292 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,10 +46,10 @@ repos: args: [--config, .pymarkdown.yaml, fix] - repo: https://github.com/astral-sh/ruff-pre-commit.git - rev: v0.15.14 + rev: v0.4.8 hooks: - # - id: ruff - # args: [--fix] + - id: ruff + args: [--fix] - id: ruff-format # Global Configuration diff --git a/ruff.toml b/.ruff.toml similarity index 100% rename from ruff.toml rename to .ruff.toml diff --git a/README.md b/README.md index 6a22793c..cb8bff25 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ class MyProjectConan(ConanFile): self.requires("sen/[>=1.0]") ``` -2. Run `conan install . --profile --build=missing` to download Sen before running CMake. +1. Run `conan install . --profile --build=missing` to download Sen before running CMake. To ensure your system is able to find all paths, add the `bin` folder to your `PATH` environment variable (in Linux also add it to the `LD_LIBRARY_PATH`). Check that everything works by running `sen shell`. You can play with the objects in the `local` session. diff --git a/apps/cli_gen/test/test19/test.py b/apps/cli_gen/test/test19/test.py index 98f1100d..d8ee2cab 100755 --- a/apps/cli_gen/test/test19/test.py +++ b/apps/cli_gen/test/test19/test.py @@ -4,24 +4,36 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Encodes a cli_gen test to ensure code gen settings are correctly handled.""" +import os import subprocess import sys -import os -# integration test to check that the codegen settings work well in FOM code generation -if __name__ == "__main__": + +def main() -> None: + """ + Encodes the setup and testing logic of running cli_gen. + + Integration test to check that the codegen settings work well in FOM code generation. + """ # input arguments exe_path = "./cli_gen" source_dir = os.path.dirname(os.path.abspath(__file__)) output_file = os.path.join(os.getcwd(), "fom", "modulea-19.xml.h") target_word = "moduleAIntAcceptsSet" - cmd = [exe_path, "cpp", "fom", f"--directories={source_dir}/fom", f"--settings={source_dir}/codegen_settings.json"] + cmd = [ + exe_path, + "cpp", + "fom", + f"--directories={source_dir}/fom", + f"--settings={source_dir}/codegen_settings.json", + ] print(f"Executing: {' '.join(cmd)}") - result = subprocess.run(cmd, capture_output=True, text=True) + result = subprocess.run(cmd, capture_output=True, text=True, check=False) if result.returncode != 0: print(f"Error: cli_gen failed with exit code {result.returncode}") @@ -33,7 +45,7 @@ print(f"Error: Generated file NOT FOUND at {output_file}") sys.exit(1) - with open(output_file, "r") as f: + with open(output_file, encoding="utf-8") as f: if target_word in f.read(): # the test passes if the skeleton method for the checked property is present print(f"Success: Found '{target_word}' in {output_file}") @@ -41,3 +53,7 @@ else: print(f"Failure: '{target_word}' not found in the file.") sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/apps/cli_gen/test/test20/test.py b/apps/cli_gen/test/test20/test.py index eb880333..d45d96c5 100755 --- a/apps/cli_gen/test/test20/test.py +++ b/apps/cli_gen/test/test20/test.py @@ -4,13 +4,19 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Encodes a cli_gen test to ensure FOM generations works correctly.""" +import os import subprocess import sys -import os -# integration test to check that the FOM generator creates Maybe types for optional parameters -if __name__ == "__main__": + +def main() -> None: + """ + Encodes the setup and testing logic of running cli_gen. + + Integration test to check that the FOM generator creates Maybe types for optional parameters + """ # input arguments exe_path = "./cli_gen" source_dir = os.path.dirname(os.path.abspath(__file__)) @@ -23,7 +29,7 @@ print(f"Executing: {' '.join(cmd)}") - result = subprocess.run(cmd, capture_output=True, text=True) + result = subprocess.run(cmd, capture_output=True, text=True, check=False) if result.returncode != 0: print(f"Error: cli_gen failed with exit code {result.returncode}") @@ -35,20 +41,26 @@ print(f"Error: Generated file NOT FOUND at {output_file}") sys.exit(1) - with open(output_file, "r") as f: + with open(output_file, encoding="utf-8") as f: content = f.read() if target_optional not in content: print( - f"Failure: Optional parameter generation failed. '{target_optional}' not found in the file {output_file}." + f"Failure: Optional parameter generation failed. '{target_optional}' " + f"not found in the file {output_file}." ) sys.exit(1) if target_required not in content: print( - f"Failure: Required parameter generation failed. '{target_required}' not found in the file {output_file}." + f"Failure: Required parameter generation failed. '{target_required}' " + f"not found in the file {output_file}." ) sys.exit(1) print(f"Success: Found correctly generated parameters in {output_file}") sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/cmake/util/color_cmake_target_graph.py b/cmake/util/color_cmake_target_graph.py index fc25e609..cc036941 100755 --- a/cmake/util/color_cmake_target_graph.py +++ b/cmake/util/color_cmake_target_graph.py @@ -4,12 +4,11 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Script to generate a colored cmake target graph.""" import re import sys -input_path, output_path = sys.argv[1], sys.argv[2] - COLORS_DATA = { "egg": {"outline": "#f9a825", "fill": "#fbc02d"}, # executables "doubleoctagon": {"outline": "#7e57c2", "fill": "#9575cd"}, # shared libs @@ -20,24 +19,40 @@ "box": {"outline": "#66bb6a", "fill": "#a5d6a7"}, # custom target } -shape_re = re.compile(r"(shape\s*=\s*\"?([a-zA-z0-9_]+)\"?)") +SHAPE_REGEX = re.compile(r"(shape\s*=\s*\"?([a-zA-z0-9_]+)\"?)") + + +def main() -> None: + """ + Defines the core logic of the script. + + 1. read inputs from cmake + 2. match shapes and color them + 3. write out the resulting graph + """ + input_path, output_path = sys.argv[1], sys.argv[2] + + with open(input_path, encoding="utf-8") as f: + lines = f.readlines() + + out_lines = [] -with open(input_path, "r", encoding="utf-8") as f: - lines = f.readlines() + for raw_line in lines: + line = raw_line + if "[" in line and "shape" in line and "]" in line: + m = SHAPE_REGEX.search(line) + if m: + shape = m.group(2) + data = COLORS_DATA.get(shape) + if data: + color = data.get("outline") + fillcolor = data.get("fill") + line = line.replace("]", f', color="{color}", fillcolor="{fillcolor}"]', 1) + out_lines.append(line) -out_lines = [] + with open(output_path, "w", encoding="utf-8") as f: + f.writelines(out_lines) -for line in lines: - if "[" in line and "shape" in line and "]" in line: - m = shape_re.search(line) - if m: - shape = m.group(2) - data = COLORS_DATA.get(shape) - if data: - color = data.get("outline") - fillcolor = data.get("fill") - line = line.replace("]", f', color="{color}", fillcolor="{fillcolor}"]', 1) - out_lines.append(line) -with open(output_path, "w", encoding="utf-8") as f: - f.writelines(out_lines) +if __name__ == "__main__": + main() diff --git a/cmake/util/generate_coverage_report.py b/cmake/util/generate_coverage_report.py index 8e1fad00..74ab5fb7 100755 --- a/cmake/util/generate_coverage_report.py +++ b/cmake/util/generate_coverage_report.py @@ -9,7 +9,6 @@ """Script for generating llvm-cov based coverage reports.""" -import typing as tp import argparse import glob import os @@ -17,7 +16,7 @@ def merge_coverage_profiles(llvm_profdata, profile_data_dir): - """Merge separate coverage prof data into one merged profile with `llvm-profdata merge`""" + """Merge separate coverage prof data into one merged profile with `llvm-profdata merge`.""" print(" ├ Merging raw profiles...") raw_profiles = glob.glob(os.path.join(profile_data_dir, "*.profraw")) @@ -25,7 +24,7 @@ def merge_coverage_profiles(llvm_profdata, profile_data_dir): input_files = os.path.join(profile_data_dir, "profile_input_files") profdata_path = os.path.join(profile_data_dir, "merged_coverage.profdata") - with open(input_files, "w") as manifest: + with open(input_files, "w", encoding="utf-8") as manifest: manifest.write("\n".join(raw_profiles)) cmd = [llvm_profdata, "merge"] @@ -42,7 +41,7 @@ def merge_coverage_profiles(llvm_profdata, profile_data_dir): return profdata_path -def transform_to_binary_args(binaries) -> tp.List[str]: +def transform_to_binary_args(binaries) -> list[str]: """Expands the binaries into a arg list that the llvm tools expect.""" binary_cmd_line = [binaries[0]] for binary in binaries[1:]: @@ -52,7 +51,7 @@ def transform_to_binary_args(binaries) -> tp.List[str]: return binary_cmd_line -def generate_html_report(llvm_cov, profile, report_dir, binaries, ignore_regex: tp.Optional[str]): +def generate_html_report(llvm_cov, profile, report_dir, binaries, ignore_regex: str | None): """Generates a html report for the given coverage data.""" print(" ├ Generating html report files...") @@ -71,7 +70,7 @@ def generate_html_report(llvm_cov, profile, report_dir, binaries, ignore_regex: subprocess.check_call(cmd) -def generate_coverage_report(llvm_cov, profile, report_dir, binaries, ignore_regex: tp.Optional[str]): +def generate_coverage_report(llvm_cov, profile, report_dir, binaries, ignore_regex: str | None): """Generates a coverage report for the given coverage data.""" print(" ├ Generating coverage report...") @@ -89,7 +88,7 @@ def generate_coverage_report(llvm_cov, profile, report_dir, binaries, ignore_reg ) -def generate_coverage_summary(llvm_cov, profile, report_dir, binaries, ignore_regex: tp.Optional[str]): +def generate_coverage_summary(llvm_cov, profile, report_dir, binaries, ignore_regex: str | None): """Generates a coverage summary for the given coverage data.""" print(" ├ Generating coverage summary...") diff --git a/components/py/test/src/modify_objects.py b/components/py/test/src/modify_objects.py index 903304b9..87f1bc48 100644 --- a/components/py/test/src/modify_objects.py +++ b/components/py/test/src/modify_objects.py @@ -4,6 +4,7 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to test the modification of objects through the py component.""" import sen @@ -17,27 +18,27 @@ def dynamic_prop_changed(): - global test_object, dynamic_value, cb_fired - - assert test_object.dynamicProp == dynamic_value, ( - f"Error in dynamicProp [value: {test_object.dynamicProp}, expectation: {dynamic_value}]" - ) + """Calback to react when a property changed.""" + assert ( + test_object.dynamicProp == dynamic_value + ), f"Error in dynamicProp [value: {test_object.dynamicProp}, expectation: {dynamic_value}]" # stopping after checking the value of the property sen.api.requestKernelStop() def run(): - global test_object, test_bus, dynamic_value, static_value + """Sen run: to setup the initial component state.""" + global test_object, test_bus # noqa: PLW0603 test_object = sen.api.make("py_test_package.TestObject", "test_object", staticProp=static_value) test_bus = sen.api.getBus("my.tutorial") test_bus.add(test_object) # check the value of the static property - assert test_object.staticProp == static_value, ( - f"Error in staticProp [value: {test_object.staticProp}, expectation: {static_value}]" - ) + assert ( + test_object.staticProp == static_value + ), f"Error in staticProp [value: {test_object.staticProp}, expectation: {static_value}]" # react to changes in the dynamicProp test_object.onDynamicPropChanged(dynamic_prop_changed) @@ -47,7 +48,8 @@ def run(): def update(): - global cycle + """Sen update: triggers test execution.""" + global cycle # noqa: PLW0603 if cycle > 1: print("Callback for dynamicProp did not trigger when expected") @@ -56,7 +58,8 @@ def update(): def stop(): - global test_bus, test_object + """Sen stop: trigger that the execution stops.""" + global test_bus, test_object # noqa: PLW0603 test_bus.remove(test_object) test_object, test_bus = None, None diff --git a/components/recorder/test/data/gen_crash_record.py b/components/recorder/test/data/gen_crash_record.py index 3dce47a0..e662a2f2 100644 --- a/components/recorder/test/data/gen_crash_record.py +++ b/components/recorder/test/data/gen_crash_record.py @@ -4,10 +4,12 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to generate crash records.""" -import sen -import signal import os +import signal + +import sen # to store the object obj = None @@ -15,13 +17,14 @@ def run(): - global obj # refer to the global variable defined above + """Sen run: to setup the initial component state.""" + global obj # refer to the global variable defined above # noqa: PLW0603 obj = sen.api.open("SELECT * FROM my.tutorial") def update(): - global obj # refer to the global variable defined above - global execution_counter + """Sen update: triggers test execution.""" + global execution_counter # refer to the global variable defined above # noqa: PLW0603 execution_counter += 1 # if the object is present diff --git a/conanfile.py b/conanfile.py index 48457971..5362c608 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,16 +4,21 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to define how to setup conan packages for Sen.""" + +from os import getenv +from os.path import isdir, join from conan import ConanFile -from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout +from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout from conan.tools.files import copy from conan.tools.scm.git import Git -from conan.tools.system.package_manager import Apt, Yum, Dnf, Zypper -from os.path import isdir, join -from os import getenv +from conan.tools.system.package_manager import Apt, Dnf, Yum, Zypper + class SenConan(ConanFile): + """Conan file that specifies how Sen can be build and packaged with conan.""" + name = "sen" author = "Enrique Parodi Spalazzi (enrique.parodi@airbus.com)" # Main PoC url = "https://github.com/airbus/sen" @@ -26,11 +31,13 @@ class SenConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def build_requirements(self): + """Defines the dependencies only need for building of Sen.""" self.tool_requires("cmake/3.28.1") self.tool_requires("ninja/1.13.2") self.test_requires("gtest/1.17.0") def requirements(self): + """Defines the dependencies of Sen.""" # non-visible dependencies self.requires("asio/1.36.0", visible=False) self.requires("cli11/2.3.2", visible=False) @@ -57,11 +64,14 @@ def requirements(self): # our usage of imgui in Linux has an implicit dependency on SDL. # SDL has a missing dependency on libext-dev, so we need to install it. if self.settings.os == "Linux": - self.requires("sdl/2.24.0", - options={"alsa": False, "pulse": False, "shared": True, "wayland": False, "libunwind": False}, - visible=False) + self.requires( + "sdl/2.24.0", + options={"alsa": False, "pulse": False, "shared": True, "wayland": False, "libunwind": False}, + visible=False, + ) def system_requirements(self): + """Defines the system requirements of Sen.""" # our usage of imgui on Linux has an implicit dependency on SDL, # which itself requires libXext. Install it via the system package manager. if self.settings.os == "Linux": @@ -71,9 +81,22 @@ def system_requirements(self): Zypper(self).install(["libxext-devel"], update=True) # opensuse, sles, ... def export_sources(self): + """Defines the set of files that should be exported for building Sen.""" # Sources are located in the same place as this recipe, copy them to the recipe - include_patterns = ["apps/*", "cmake/*", "components/*", "examples/*", "libs/*", "test/*", - "CMakeLists.txt", ".clang-tidy", ".clang-format", "LICENSE.txt", "util/*", "resources/*"] + include_patterns = [ + "apps/*", + "cmake/*", + "components/*", + "examples/*", + "libs/*", + "test/*", + "CMakeLists.txt", + ".clang-tidy", + ".clang-format", + "LICENSE.txt", + "util/*", + "resources/*", + ] exclude_patterns = ["*/__pycache__/*", "*/.mypy_cache/*", "doc/*", "*/schema.json"] @@ -84,7 +107,8 @@ def export_sources(self): no_copy_source = True def set_version(self): - if isdir(join(self.recipe_folder, '.git')): + """Defines the version number that should be used.""" + if isdir(join(self.recipe_folder, ".git")): # sets project version either to the current tag or the # last tag with with the diff-commit addition. # For example: @@ -107,9 +131,11 @@ def set_version(self): self.version = fictive_version def layout(self): + """Define the folder layout for building Sen.""" cmake_layout(self) def generate(self): + """Generate the cmake dependency and toolchain files.""" deps = CMakeDeps(self) deps.generate() @@ -138,15 +164,18 @@ def generate(self): tc.generate() def build(self): + """Configure and build Sen.""" cmake = CMake(self) cmake.configure() cmake.build() def package(self): + """Package Sen into a conan package.""" cmake = CMake(self) cmake.install() def package_info(self): + """Calculate the conan package info.""" self.cpp_info.set_property("cmake_find_mode", "none") self.cpp_info.builddirs = [join("cmake", "sen")] self.cpp_info.set_property("cmake_target_name", "sen::core sen::kernel sen::db sen::util") @@ -160,11 +189,14 @@ def package_info(self): # Windows: PATH is already prepended above; no additional loader path needed. + def env_var_to_bool(env_var_name): """Returns True if the environment variable is set to a truthy value, False otherwise.""" return getenv(env_var_name, "").lower() in ("true", "on", "1", "yes") + def select_sanitizer() -> str: + """Determine which sanitizer should be enabled.""" if env_var_to_bool("ENABLE_ASAN"): print("Configuring address and undefined-behavior sanitizers...") return "ASanUBSan" diff --git a/examples/apps/rest_python/sen_client.py b/examples/apps/rest_python/sen_client.py index 1c7f948e..66531793 100644 --- a/examples/apps/rest_python/sen_client.py +++ b/examples/apps/rest_python/sen_client.py @@ -4,18 +4,18 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== - -"""This program contains an example of how users can interact with the REST API using Python. +""" +This program contains an example of how users can interact with the REST API using Python. It shows how to sign in, create interests, display details and interact with remote objects. """ -import requests import json -import sseclient +import threading import time -from typing import List, Dict, Any +import requests +import sseclient class SenClient: @@ -30,6 +30,12 @@ class SenClient: """ def __init__(self, base_url="http://localhost"): + """ + Initialize SenClient. + + Args: + base_url: base url to use (defaults to localhost) + """ self.base_url = base_url.rstrip("/") self.session = requests.Session() self.sse_event_types = [ @@ -143,8 +149,8 @@ def post_args(self, href, args_str): if args_str: args = [] - for v in args_str.split(","): - v = v.strip() + for raw_v in args_str.split(","): + v = raw_v.strip() try: args.append(int(v)) except ValueError: @@ -157,7 +163,6 @@ def post_args(self, href, args_str): def enable_notifications(self): """Enable notifications that are displayed on standard output.""" - import threading def listen_sse(): """Tries to enable notifications and displays them on the standard output. @@ -179,7 +184,7 @@ def listen_sse(): print("Notifications enabled") def disable_notifications(self): - """Disable notifications if they are enabled""" + """Disable notifications if they are enabled.""" if self.sse_thread: self.sse_thread = None print("SSE disabled") @@ -300,7 +305,7 @@ def print_objects(self, objects): """Display all the objects contained on an interest on standard output. Args: - interests (list[dict]): List containing all the objects. + objects (list[dict]): List containing all the objects. """ print("\n=== Objects ===") for obj in objects: @@ -309,13 +314,16 @@ def print_objects(self, objects): print() def get_object(self, interest_name, object_name): - """Get all the object details. + """ + Get all the object details. Args: + interest_name: Name of the interest object_name (str): Name of the object. Returns: - dict: Dictionary with all the object with its class name, description, links, local name, name and object id. + dict: Dictionary with all the object associated with its class name, description, links, + local name, name and object id. Raises: requests.RequestException: If there is any error in the request. @@ -324,7 +332,8 @@ def get_object(self, interest_name, object_name): return data def print_object(self, data): - """Display all the object details. + """ + Display all the object details. Args: data (dict): Dictionary containing all the object details. @@ -350,19 +359,13 @@ def print_object(self, data): print("\n=== Object Details ===") if methods: - print("\nMethods:") - for method in methods: - print(f" {method}") + print("\nMethods:" + "\n".join(f" {method}" for method in methods)) if properties: - print("\nProperties:") - for prop in properties: - print(f" {prop}") + print("\nProperties:" + "\n".join(f" {prop}" for prop in properties)) if events: - print("\nEvents:") - for event in events: - print(f" {event}") + print("\nEvents:" + "\n".join(f" {event}" for event in events)) print() def get_method_info(self, interest_name, object_name, method): diff --git a/examples/config/10_python/scripts/creating_objects.py b/examples/config/10_python/scripts/creating_objects.py index 9c64e345..f2db9ab3 100644 --- a/examples/config/10_python/scripts/creating_objects.py +++ b/examples/config/10_python/scripts/creating_objects.py @@ -4,6 +4,7 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to create objects.""" import sen @@ -11,7 +12,8 @@ def run(): - global myObject, testBus # refer to the globals defined above + """Sen run: to setup the initial component state.""" + global myObject, testBus # refer to the globals defined above # noqa: PLW0603 type = { "entityKind": 1, @@ -25,7 +27,7 @@ def run(): id = {"entityNumber": 1, "federateIdentifier": {"siteID": 1, "applicationID": 1}} - print(f"Python: creating and publishing the object") + print("Python: creating and publishing the object") myObject = sen.api.make( "aircrafts.DummyAircraft", "myAircraft", entityType=type, alternateEntityType=type, entityIdentifier=id ) @@ -37,11 +39,13 @@ def run(): def update(): + """Sen update: triggers test execution.""" print(myObject) def stop(): - global testBus, myObject # refer to the globals defined above + """Sen stop: trigger that the execution stops.""" + global testBus, myObject # refer to the globals defined above # noqa: PLW0603 print("Python: deleting the object") testBus.remove(myObject) diff --git a/examples/config/10_python/scripts/hello_python.py b/examples/config/10_python/scripts/hello_python.py index 2affaf1c..9b04526e 100644 --- a/examples/config/10_python/scripts/hello_python.py +++ b/examples/config/10_python/scripts/hello_python.py @@ -4,22 +4,26 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to use the python component.""" import sen # this is executed only once (at the start of the component execution) def run(): - print(f"Python: run") + """Sen run: to setup the initial component state.""" + print("Python: run") print(f"Python: the config is: {sen.api.config}") print(f"Python: the app name is: {sen.api.appName}") # this is executed every cycle def update(): + """Sen update: triggers test execution.""" print(f"Python: update (current time: {sen.api.time})") # this is executed only once (at the end of the component execution) def stop(): - print(f"Python: stop called") + """Sen stop: trigger that the execution stops.""" + print("Python: stop called") diff --git a/examples/config/10_python/scripts/inspecting_objects.py b/examples/config/10_python/scripts/inspecting_objects.py index 34b4602d..8d120dab 100644 --- a/examples/config/10_python/scripts/inspecting_objects.py +++ b/examples/config/10_python/scripts/inspecting_objects.py @@ -4,6 +4,7 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to inspect objects.""" import sen @@ -12,7 +13,8 @@ def run(): - global list # refer to the global variable defined above + """Sen run: to setup the initial component state.""" + global list # refer to the global variable defined above # noqa: PLW0603 list = sen.api.open("SELECT * FROM local.kernel") # open it @@ -22,9 +24,7 @@ def run(): def update(): - # refer to the global variable defined above - global list - + """Sen update: triggers test execution.""" print(f"Python: printing the list at: {sen.api.time})") print(list) diff --git a/examples/config/10_python/scripts/interacting_with_objects.py b/examples/config/10_python/scripts/interacting_with_objects.py index 6ed10801..c4eb94a6 100644 --- a/examples/config/10_python/scripts/interacting_with_objects.py +++ b/examples/config/10_python/scripts/interacting_with_objects.py @@ -4,6 +4,7 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to interact with objects.""" import sen @@ -12,12 +13,13 @@ def run(): - global obj # refer to the global variable defined above + """Sen run: to setup the initial component state.""" + global obj # refer to the global variable defined above # noqa: PLW0603 obj = sen.api.open('SELECT * FROM local.shell WHERE name = "shell_impl"') def update(): - global obj # refer to the global variable defined above + """Sen update: triggers test execution.""" print("Python: update") # if the object is present, do something with it diff --git a/examples/config/10_python/scripts/reacting_to_events_and_changes.py b/examples/config/10_python/scripts/reacting_to_events_and_changes.py index f1b95ade..342afb45 100644 --- a/examples/config/10_python/scripts/reacting_to_events_and_changes.py +++ b/examples/config/10_python/scripts/reacting_to_events_and_changes.py @@ -4,6 +4,7 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to react on events and changes (e.g., object added).""" import sen @@ -11,16 +12,18 @@ teachers = None -def teacherDetected(teacher): +def teacherDetected(teacher) -> None: + """Prints information about the detected teacher.""" teacher.onStressLevelPeaked(lambda args: print(f"Python: {teacher.name} stress level peaking to {args}")) teacher.onStatusChanged(lambda: print(f"Python: {teacher.name} status changed to {teacher.status}")) -def run(): - global teachers # refer to the global variable defined above +def run() -> None: + """Sen run: to setup the initial component state.""" + global teachers # refer to the global variable defined above # noqa: PLW0603 print("Python: run") # select the object and install some callbacks teachers = sen.api.open("SELECT school.Teacher FROM school.primary") - teachers.onAdded(lambda obj: teacherDetected(obj)) + teachers.onAdded(teacherDetected) diff --git a/examples/config/6_recorder/3_recorder_school_print.py b/examples/config/6_recorder/3_recorder_school_print.py index 0d36ab5c..916d717b 100644 --- a/examples/config/6_recorder/3_recorder_school_print.py +++ b/examples/config/6_recorder/3_recorder_school_print.py @@ -4,10 +4,12 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to handle recording.""" -import sen_db_python as sen from datetime import datetime +import sen_db_python as sen + epoch = datetime(1970, 1, 1) try: diff --git a/libs/kernel/test/integration/crash_report/crash_report_tester.py b/libs/kernel/test/integration/crash_report/crash_report_tester.py index 7209425e..761ca30f 100644 --- a/libs/kernel/test/integration/crash_report/crash_report_tester.py +++ b/libs/kernel/test/integration/crash_report/crash_report_tester.py @@ -4,15 +4,17 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to specify the crash reporter test cases.""" -import subprocess import json import os -import re import platform +import re +import subprocess def test_crash_reporter_generates_stacktrace(): + """Test to ensure that the crash reporter generates a stacktrace on failure.""" config_path = os.path.join(os.path.dirname(__file__), "config", "config.yaml") sen_executable = "sen" if platform.system() == "Windows" else "./sen" @@ -28,7 +30,7 @@ def test_crash_reporter_generates_stacktrace(): assert os.path.exists(report_path), f"Crash report file does not exist: {report_path}" try: - with open(report_path, "r") as f: + with open(report_path, encoding="utf-8") as f: data = json.load(f) error_data = data.get("errorData", {}) diff --git a/libs/kernel/test/integration/object_sync/run.py b/libs/kernel/test/integration/object_sync/run.py index 121da58e..1d4083e6 100644 --- a/libs/kernel/test/integration/object_sync/run.py +++ b/libs/kernel/test/integration/object_sync/run.py @@ -4,22 +4,20 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module that implements the container test harness runner.""" # std import os -import re import sys import time -import docker -import getpass -import subprocess from pathlib import Path from threading import Thread -from docker.models.containers import Container as WrappedContainer + +import docker +from testcontainers.core.container import DockerContainer # testcontainers from testcontainers.core.network import Network -from testcontainers.core.container import DockerContainer # constants # TODO (SEN-1681) replace with a lighter runtime image @@ -28,7 +26,7 @@ def stream_logs(cont: DockerContainer) -> None: - """formats and streams logs from the containers""" + """Formats and streams logs from the containers.""" w = cont.get_wrapped_container() for line in w.logs(stream=True, follow=True): if line: @@ -36,17 +34,17 @@ def stream_logs(cont: DockerContainer) -> None: def is_container() -> bool: - """checks whether the script is running inside a container""" + """Checks whether the script is running inside a container.""" try: return os.path.exists("/.dockerenv") or any( - k in open("/proc/self/cgroup", "rt").read() for k in ("docker", "containerd") + k in open("/proc/self/cgroup", encoding="utf-8").read() for k in ("docker", "containerd") ) except (FileNotFoundError, PermissionError): return False def get_repo_root(start_path: Path) -> Path: - """returns the repo root path from a given path""" + """Returns the repo root path from a given path.""" for p in [start_path] + list(start_path.parents): if (p / ".git").exists(): return p @@ -54,7 +52,7 @@ def get_repo_root(start_path: Path) -> Path: def find_host_mount(container_path: Path) -> Path | None: - """finds the directory in the host where a certain container path is mounted""" + """Finds the directory in the host where a certain container path is mounted.""" if not (mount_file := Path("/proc/self/mountinfo")).exists(): raise FileNotFoundError("Could not access /proc/self/mountinfo. Host might not be a docker") @@ -63,7 +61,7 @@ def find_host_mount(container_path: Path) -> Path | None: mounts = list( { Path(ws if ws else parts[3]) - for line in open(mount_file, "r") + for line in open(mount_file, encoding="utf-8") if str(container_path) in line and (parts := line.split()) } ) @@ -74,20 +72,25 @@ def find_host_mount(container_path: Path) -> Path | None: def check_image_availability(image: str) -> None: - """reports an error if the container image is not found in the docker cache""" + """Reports an error if the container image is not found in the docker cache.""" try: client = docker.from_env() client.images.get(image) - except docker.errors.ImageNotFound: - raise RuntimeError("Container Image not found in the local cache. Please pull the image first.") + except docker.errors.ImageNotFound as err: + raise RuntimeError("Container Image not found in the local cache. Please pull the image first.") from err except docker.errors.DockerException as e: - raise RuntimeError(f"Could not connect to the local Docker daemon: {e}") + raise RuntimeError("Could not connect to the local Docker daemon.") from e def abort(container_list: list[DockerContainer], thread_list: list[Thread]) -> None: - """stops containers and joins log threads before aborting with error""" - list(map(lambda elem: elem.stop(), container_list)) - list(map(Thread.join, [t for t in thread_list if isinstance(t, Thread)])) + """Stops containers and joins log threads before aborting with error.""" + for container in container_list: + container.stop() + + for t in thread_list: + if isinstance(t, Thread): + t.join() + sys.exit(1) @@ -137,7 +140,8 @@ def abort(container_list: list[DockerContainer], thread_list: list[Thread]) -> N deadline = time.time() + TIMEOUT while time.time() < deadline: wrapped = [c.get_wrapped_container() for c in containers] - list(map(lambda w: w.reload(), wrapped)) + for w in wrapped: + w.reload() # abort if any of the processes has exited with error if any(w.status == "exited" and w.wait()["StatusCode"] != 0 for w in wrapped): diff --git a/libs/kernel/test/integration/runner.py b/libs/kernel/test/integration/runner.py index a73151ee..2150a579 100644 --- a/libs/kernel/test/integration/runner.py +++ b/libs/kernel/test/integration/runner.py @@ -4,21 +4,28 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to orchestrate multiple sen processes to run the test setup.""" +import os import subprocess import sys -import os -from time import sleep -def run_sen_command(arg): +def run_sen_command(args): + """ + Do a sen run with the given arguments. + + Args: + args: passed to sen + """ if os.name == "nt": # Windows - subprocess.Popen(["sen", "run", arg], start_new_session=True, env=os.environ.copy()) + subprocess.Popen(["sen", "run", args], start_new_session=True, env=os.environ.copy()) else: # Unix-like - subprocess.Popen(["./sen", "run", arg], start_new_session=True) + subprocess.Popen(["./sen", "run", args], start_new_session=True) def main(): + """Run the test setup.""" if len(sys.argv) != 4: print("Usage: python runner.py ") sys.exit(1) diff --git a/libs/kernel/test/integration/tester.py b/libs/kernel/test/integration/tester.py index cb375602..a9b53ee7 100644 --- a/libs/kernel/test/integration/tester.py +++ b/libs/kernel/test/integration/tester.py @@ -4,21 +4,22 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== -import sys -import time -from abc import ABC, abstractmethod +"""Module to run the generic integration testes as a Sen component.""" + import sen -from datetime import datetime -class TesterBase(ABC): +class TesterBase: + """Tester base for generic testing functions.""" + def __init__(self, name, sen_api): """ Base testing class to reproduce tests on an iterative loop inside a Python Sen component. - :param name: Name of the test to run - :param sen_api: Python Sen api used in the component. - """ + Args: + name: Name of the test to run + sen_api: Python Sen api used in the component. + """ self._name = name self.__api = sen_api self.__start_time = self.__api.time @@ -31,13 +32,12 @@ def __init__(self, name, sen_api): self.__has_failed = False def mark_as_failed(self): + """Marks the tests state as failed and stops test execution.""" self.__has_failed = True self.__api.requestKernelStop(1) def get_test_elapsed_seconds(self): - """ - Get the seconds passed since the beginning of the test. - """ + """Get the seconds passed since the beginning of the test.""" if self.__start_time.total_seconds() == 0: self.__start_time = self.__api.time return (self.__api.time - self.__start_time).total_seconds() @@ -45,9 +45,11 @@ def get_test_elapsed_seconds(self): def set_test(self, test_name, test_func, test_condition): """ Create a test to be run during the testing process. - :param test_name: Name of the test - :param test_func: Function that contains the logic of the test to execute - :param test_condition: Condition which will trigger the test. + + Args: + test_name: Name of the test + test_func: Function that contains the logic of the test to execute + test_condition: Condition which will trigger the test. """ self.__tests[test_name] = test_func self.__test_conditions[test_name] = test_condition @@ -57,6 +59,7 @@ def set_test(self, test_name, test_func, test_condition): def execute_test(self, test_name): """ Execute a given test if it hasn't been run already and its condition has been met. + Store test result inside the test dictionary and manage the assertion error in case the test fails. """ if ( @@ -71,8 +74,9 @@ def execute_test(self, test_name): def run_tests(self): """ - Iterative method that runs the specified tests, checking the possible failures on assertions and marking the - component to stop in failure case. + Iterative method that runs the specified tests. + + This methods is checking the possible failures on assertions and marking the component to stop in failure case. """ for test_name in self.__tests.keys(): self.execute_test(test_name) @@ -81,10 +85,15 @@ def run_tests(self): class KernelTransportTester(TesterBase): + """Tester class to execute the kernel transport tests.""" + def set_tests(self): + """Registers the test functions.""" + def test_condition(): - "Check that both tester objects are ready prior to executing the test" - global object_list, tester1, tester2 + """Check that both tester objects are ready prior to executing the test.""" + # TODO (SEN-1689): clean up global state dependence + global tester1, tester2 # noqa: PLW0603 expected_names = {"tester1", "tester2", "obj1", "obj2"} objects_present = len(object_list) == 4 and {obj.name for obj in object_list} == expected_names @@ -103,48 +112,36 @@ def test_condition(): return objects_present and testers_ready def abort_tests(): - global tester1, tester2 - tester1.shutdownKernel() tester2.shutdownKernel() self.mark_as_failed() def check_test_4(result): - global tester1, tester2 - assert not result, abort_tests() tester1.shutdownKernel() tester2.shutdownKernel() sen.api.requestKernelStop(0) def check_test_3(result): - global tester1 - assert not result, abort_tests() - print(f"[tester] calling tester1.checkLocalState") - tester1.checkLocalState(lambda args: check_test_4(args)) + print("[tester] calling tester1.checkLocalState") + tester1.checkLocalState(check_test_4) def check_test_2(result): - global tester2 - assert not result, abort_tests() - print(f"[tester] calling tester2.doTests") - tester2.doTests(lambda args: check_test_3(args)) + print("[tester] calling tester2.doTests") + tester2.doTests(check_test_3) def check_test_1(result): - global tester2 - assert not result, abort_tests() - print(f"[tester] calling tester2.checkLocalState") - tester2.checkLocalState(lambda args: check_test_2(args)) + print("[tester] calling tester2.checkLocalState") + tester2.checkLocalState(check_test_2) def test_body(): - """Check setting of remote properties between two kernel instances""" - global tester1 - + """Check setting of remote properties between two kernel instances.""" # test setting remote properties in the forward direction - print(f"[tester] calling tester1.doTests") - tester1.doTests(lambda args: check_test_1(args)) + print("[tester] calling tester1.doTests") + tester1.doTests(check_test_1) self.set_test("transport_test", test_body, test_condition) @@ -157,16 +154,17 @@ def test_body(): def run(): - global tester, object_list + """Sen run: to setup the initial component state.""" + # TODO (SEN-1689): clean up global state dependence + global tester, object_list # noqa: PLW0603 object_list = sen.api.open("SELECT * FROM session.bus") - print(f"[tester] creating obj list with interest SELECT * FROM session.bus") + print("[tester] creating obj list with interest SELECT * FROM session.bus") tester = KernelTransportTester("transport_tester", sen.api) tester.set_tests() def update(): - global tester, object_list - + """Sen update: triggers test execution.""" tester.run_tests() diff --git a/libs/kernel/test/integration/type_clash/type_clash_tester.py b/libs/kernel/test/integration/type_clash/type_clash_tester.py index b38cf8dd..a70150e7 100644 --- a/libs/kernel/test/integration/type_clash/type_clash_tester.py +++ b/libs/kernel/test/integration/type_clash/type_clash_tester.py @@ -4,25 +4,25 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to run the type clash tests as a Sen component.""" import sen from tester import TesterBase class TypeClashTester(TesterBase): + """Tester class to execute the type clash tests.""" + def set_tests(self): + """Registers the test functions.""" + def test_condition(): return self.get_test_elapsed_seconds() > 2.5 def test_body(): - global object_list - for obj in object_list: if "obj_app_" in obj.name: - try: - obj.shutdownKernel() - except: - pass + obj.shutdownKernel() sen.api.requestKernelStop(0) @@ -34,12 +34,14 @@ def test_body(): def run(): - global tester, object_list + """Sen run: to setup the initial component state.""" + # TODO (SEN-1689): clean up global state dependence + global tester, object_list # noqa: PLW0603 object_list = sen.api.open("SELECT * FROM session.bus") tester = TypeClashTester("type_clash_tester", sen.api) tester.set_tests() def update(): - global tester + """Sen update: triggers test execution.""" tester.run_tests()