diff --git a/.gitignore b/.gitignore index b97b08f..92476da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,11 @@ # CLion folders /.idea /cmake-build-* - +/build* # guided_examples products /docs/guided_examples/*/build* /docs/guided_examples/flake.lock -# Avoid adding symlinks -/support/ides/clion/* - # Products of SimoSim Simo.log statistics.yaml diff --git a/CMakeLists.txt b/CMakeLists.txt index 733f115..7f5dc25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,7 @@ cmake_minimum_required(VERSION 3.31.0) project(Simo VERSION 0.0.1) include(CPack) -set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) option(PORTABLE_BUILD "Be verbose on build and turn warnings into errors for the sake of build portability" ON) @@ -199,6 +199,11 @@ target_sources(test_Parameter PRIVATE tests/parameter/ParameterTest.cc ) +create_unit_test_executable(test_Port) +target_sources(test_Port PRIVATE + tests/port/PortTest.cc +) + create_unit_test_executable(test_Statistics) target_sources(test_Statistics PRIVATE tests/statistics/StatisticsTest.cc diff --git a/flake.nix b/flake.nix index c39ed77..8c7daa0 100644 --- a/flake.nix +++ b/flake.nix @@ -87,9 +87,9 @@ }; derivationAttributes = { - default = pkgs.clangStdenv.mkDerivation simoBaseAttributes; - clang = pkgs.clangStdenv.mkDerivation simoBaseAttributes; - gcc = pkgs.gccStdenv.mkDerivation simoBaseAttributes; + default = pkgs.clangStdenv.mkDerivation simoBaseAttributes; + clang = pkgs.clangStdenv.mkDerivation simoBaseAttributes; + gcc = pkgs.gccStdenv.mkDerivation simoBaseAttributes; }; in { @@ -106,11 +106,16 @@ { inputsFrom = [ derivationAttributes.default ]; packages = with pkgs; [ + git + bashInteractive clang-tools ast-grep # For llvm-cov llvmPackages.llvm ]; + shellHook = '' + export SHELL="${pkgs.bashInteractive}/bin/bash" + ''; }; } ); diff --git a/include/Simo/port/Port.h b/include/Simo/port/Port.h index 1772d13..f0f10e5 100644 --- a/include/Simo/port/Port.h +++ b/include/Simo/port/Port.h @@ -18,6 +18,8 @@ #define SIMO_PORT_HH #include +#include +#include #include "Simo/compiler/BoostTypeIndexRuntimeCast.h" #include "Simo/compiler/Compiler.h" @@ -53,6 +55,9 @@ namespace Ports { template class InPort; +template +class CallbackOutPort; + /// Templated port that can send payloads to an InPort of the same type /// /// Present the payload to the connected port with send and clear that state @@ -89,6 +94,19 @@ class SIMO_PUBLIC OutPort : public Port { std::abort(); } + SEND_OUTCOME send(Payload& payload) { + storage = payload; + switch (state_) { + case PORT_STATE::EMPTY: + state_ = PORT_STATE::FILLED; + return SEND_OUTCOME::NEW; + case PORT_STATE::FILLED: + state_ = PORT_STATE::FILLED; + return SEND_OUTCOME::REPLACED; + } + std::abort(); + } + void clear() { state_ = PORT_STATE::EMPTY; } PORT_STATE state() const { return state_; } @@ -132,6 +150,91 @@ class SIMO_PUBLIC InPort : public Port { OutPort* connecting_port = nullptr; }; +/// Templated output port that receives payloads from a CallbackInPort of the +/// same type and invokes a callback for each payload. +template +class SIMO_PUBLIC CallbackInPort : public Port { + public: + friend class CallbackOutPort; + + using Callback = std::function; + + CallbackInPort() = default; + explicit CallbackInPort(Callback callback) : callback_(std::move(callback)) {} + + BOOST_TYPE_INDEX_REGISTER_RUNTIME_CLASS(Port) + [[nodiscard]] + bool connect(Port* other) override; + + void callback(Callback callback) { callback_ = std::move(callback); } + + [[nodiscard]] + bool has_callback() const { + return static_cast(callback_); + } + + protected: + void receive(Payload payload) + requires(std::is_same_v) + { + if (callback_) { + callback_(std::forward(payload)); + } + } + + [[nodiscard]] + std::optional receive(Payload payload) + requires(!std::is_same_v) + { + if (callback_) { + return callback_(std::forward(payload)); + } + return std::nullopt; + } + + Callback callback_; +}; + +/// Templated input port that sends payloads to a CallbackOutPort of the same +/// type. +template +class SIMO_PUBLIC CallbackOutPort : public Port { + public: + friend class CallbackInPort; + BOOST_TYPE_INDEX_REGISTER_RUNTIME_CLASS(Port) + [[nodiscard]] + bool connect(Port* other) override; + + [[nodiscard]] + std::optional send(Payload&& payload) { + if (connecting_port == nullptr) { + return std::nullopt; + } + return connecting_port->receive(std::forward(payload)); + } + + void send(const Payload&& payload) + requires(std::is_same_v) + { + if (connecting_port != nullptr) { + connecting_port->receive(std::forward(payload)); + } + } + + [[nodiscard]] + std::optional send(const Payload& payload) + requires(!std::is_same_v) + { + if (connecting_port != nullptr) { + return connecting_port->receive(payload); + } + return std::nullopt; + } + + protected: + CallbackInPort* connecting_port = nullptr; +}; + template bool OutPort::connect(Port* other) { if (other->get_type_id() != get_type_id()) { @@ -158,6 +261,36 @@ bool InPort::connect(Port* other) { return true; } +template +bool CallbackOutPort::connect(Port* other) { + if (other == nullptr || other->get_type_id() != get_type_id()) { + return false; + } + auto* other_casted = + boost::typeindex::runtime_cast*>( + other); + if (other_casted == nullptr) { + return false; + } + connecting_port = other_casted; + return true; +} + +template +bool CallbackInPort::connect(Port* other) { + if (other == nullptr || other->get_type_id() != get_type_id()) { + return false; + } + auto* other_casted = + boost::typeindex::runtime_cast*>( + other); + if (other_casted == nullptr) { + return false; + } + other_casted->connecting_port = this; + return true; +} + /// Port that can send and receive payloads on separate channels. /// It can be connected to a BidirectionalPortTyped (note /// the types are inverted). @@ -172,6 +305,10 @@ class SIMO_PUBLIC BidirectionalPortTyped : public Port { return out_port.send(std::move(payload)); } + OutPort::SEND_OUTCOME send_out(OutPayload& payload) { + return out_port.send(payload); + } + void clear_out() { out_port.clear(); } void clear_in() { in_port.clear(); } @@ -205,4 +342,4 @@ bool BidirectionalPortTyped::connect(Port* other) { } // namespace Simo -#endif // SIMO_PORT_HH \ No newline at end of file +#endif // SIMO_PORT_HH diff --git a/support/ides/clion/README.md b/support/ides/clion/README.md index 66678d1..0c964f2 100644 --- a/support/ides/clion/README.md +++ b/support/ides/clion/README.md @@ -1,11 +1,10 @@ # Use Nix packages in CLion -Based on [this](https://gist.github.com/pmenke-de/2fed80213c48c2fe80891678f4fa3b42), -but reworked to use flakes. +From the terminal, open a development environment with `nix develop`. -1. Run `support/ides/clion/setup-symlinks.sh` to expose some nix binaries -2. In `Settings` -> `Build, Execution, Deployment` -> `Toolchains`, create a new toolchain -3. Set CMake executable to `support/ides/clion/nix-cmake.sh` and the other elements of the -toolchain to the symlinks created in step 1. +Inside the development environment, launch CLion. -Note it is important to set the symlink of ctest to be able to run unit-tests from CLion. \ No newline at end of file +On macOS, this can be done with `open -na "CLion.app"`. + +One issue is that the integrated terminal may complain about `bash: bind: command not found` +and show escape symbol. The solution is to run `nix develop` in the terminal. \ No newline at end of file diff --git a/support/ides/clion/nix-cmake.sh b/support/ides/clion/nix-cmake.sh deleted file mode 100755 index d5fcb49..0000000 --- a/support/ides/clion/nix-cmake.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env sh -# -# Copyright 2026 Matteo Fusi and Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# Use the cmakeFlags set by Nix - this doesn't work with --build -#FLAGS=$(echo "$@" | grep -e '--build' > /dev/null || echo "$cmakeFlags") -#"$(dirname "$0")"/nix-run.sh cmake ${FLAGS:+"$FLAGS"} "$@" - - -SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd) -PROJECT_DIR=$SCRIPT_DIR/../../../ -set -x -nix develop "$PROJECT_DIR" --command "$SCRIPT_DIR/nix-run.sh" cmake "$@" -exit $? - - diff --git a/support/ides/clion/nix-run.sh b/support/ides/clion/nix-run.sh deleted file mode 100755 index 2486b78..0000000 --- a/support/ides/clion/nix-run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env sh -# -# Copyright 2026 Matteo Fusi and Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -set -x -"$@" \ No newline at end of file diff --git a/support/ides/clion/setup-symlinks.sh b/support/ides/clion/setup-symlinks.sh deleted file mode 100755 index 2d426a7..0000000 --- a/support/ides/clion/setup-symlinks.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env sh -# -# Copyright 2026 Matteo Fusi and Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd) - -# Run bash inside dev shell to create symlinks -nix develop --command bash -c " - ln -sf $(which ninja) $SCRIPT_DIR/ - ln -sf $(which clang) $SCRIPT_DIR/ - ln -sf $(which clang++) $SCRIPT_DIR/ - ln -sf $(which ctest) $SCRIPT_DIR/ -" diff --git a/tests/Simo/MainLoopTests.cc b/tests/Simo/MainLoopTests.cc index 1baa879..6cdc6c2 100644 --- a/tests/Simo/MainLoopTests.cc +++ b/tests/Simo/MainLoopTests.cc @@ -17,6 +17,7 @@ #include "support/BoostInclude.h" +namespace Simo::Tests { class TestModule : public Simo::Module { public: Simo::InitializationStatus initialize(Simo::Context& ctx, @@ -97,3 +98,4 @@ BOOST_AUTO_TEST_CASE(InitializationFailure) { ss << initialize_success; BOOST_CHECK_EQUAL(ss.str(), initialization_success_str); } +} // namespace Simo::Tests diff --git a/tests/Simo/SimulationContextTest.cc b/tests/Simo/SimulationContextTest.cc index 22fb28d..7492042 100644 --- a/tests/Simo/SimulationContextTest.cc +++ b/tests/Simo/SimulationContextTest.cc @@ -22,6 +22,7 @@ #include "support/BoostInclude.h" +namespace Simo::Tests { class InitTrackingModule final : public Simo::Module { public: Simo::InitializationStatus initialize( @@ -263,3 +264,4 @@ BOOST_AUTO_TEST_CASE(ModuleParametersGetSubtreeHitAndMiss) { auto missing = params.get_subtree("missing"); BOOST_CHECK_EQUAL(missing.has_value(), false); } +} // namespace Simo::Tests diff --git a/tests/collection/CollectionTest.cc b/tests/collection/CollectionTest.cc index 4c44f22..134a9d6 100644 --- a/tests/collection/CollectionTest.cc +++ b/tests/collection/CollectionTest.cc @@ -28,6 +28,7 @@ #define DYNLIB_EXT "so" #endif +namespace Simo::Tests { namespace { std::filesystem::path collection_library_path() { // Assuming tests are run from the build folder @@ -264,3 +265,4 @@ BOOST_AUTO_TEST_CASE(collection_with_lib_move_assignment_and_self_move) { BOOST_CHECK_EQUAL(destination.get_collection(), source_collection); } +} // namespace Simo::Tests diff --git a/tests/core/RadixHeap/RadixHeapTest.cc b/tests/core/RadixHeap/RadixHeapTest.cc index f2370cf..cd35ab8 100644 --- a/tests/core/RadixHeap/RadixHeapTest.cc +++ b/tests/core/RadixHeap/RadixHeapTest.cc @@ -17,7 +17,8 @@ #include "support/BoostInclude.h" -using Simo::Internal::RadixHeap; +namespace Simo::Tests { +using Internal::RadixHeap; BOOST_AUTO_TEST_CASE(pushElements) { RadixHeap heap; @@ -96,3 +97,4 @@ BOOST_AUTO_TEST_CASE(pushElementReorder) { BOOST_CHECK_EQUAL(heap.peek(), 0); BOOST_CHECK_EQUAL(heap.size(), 1); } +} // namespace Simo::Tests diff --git a/tests/core/TimePeriod/TimePeriodTest.cc b/tests/core/TimePeriod/TimePeriodTest.cc index 7cb903b..b43ac78 100644 --- a/tests/core/TimePeriod/TimePeriodTest.cc +++ b/tests/core/TimePeriod/TimePeriodTest.cc @@ -20,6 +20,7 @@ #include "Simo/Simo.h" #include "support/BoostInclude.h" +namespace Simo::Tests { BOOST_AUTO_TEST_CASE(TimeUnitConversions) { using Simo::Time; @@ -110,3 +111,4 @@ BOOST_AUTO_TEST_CASE(TimeJsonSerializationAndParsing) { BOOST_CHECK(invalid_error); BOOST_CHECK_EQUAL(unchanged.to_picoseconds(), 99U); } +} // namespace Simo::Tests diff --git a/tests/module/ModuleTest.cc b/tests/module/ModuleTest.cc index f190dab..9461d4a 100644 --- a/tests/module/ModuleTest.cc +++ b/tests/module/ModuleTest.cc @@ -24,6 +24,7 @@ namespace fs = std::filesystem; +namespace Simo::Tests { namespace { [[nodiscard]] std::string read_file_contents( @@ -121,3 +122,4 @@ BOOST_AUTO_TEST_CASE(ModuleChild) { BOOST_CHECK_EQUAL(child_status.success(), true); BOOST_CHECK_EQUAL(p_child.name(), "root/child"); } +} // namespace Simo::Tests diff --git a/tests/parameter/ParameterTest.cc b/tests/parameter/ParameterTest.cc index c6ee045..98a1e60 100644 --- a/tests/parameter/ParameterTest.cc +++ b/tests/parameter/ParameterTest.cc @@ -21,6 +21,7 @@ #include "support/BoostInclude.h" +namespace Simo::Tests { BOOST_AUTO_TEST_CASE(ParameterTyped_default_constructor_and_setters) { using Simo::Parameter::ParameterTyped; @@ -172,3 +173,4 @@ BOOST_AUTO_TEST_CASE(ParameterTrie_subtrie_lookup_branches) { BOOST_CHECK_EQUAL(nested_value, &value_param); BOOST_CHECK_CLOSE(nested_value->value(), 3.14, 0.0001); } +} // namespace Simo::Tests \ No newline at end of file diff --git a/tests/port/PortTest.cc b/tests/port/PortTest.cc new file mode 100644 index 0000000..079f3a1 --- /dev/null +++ b/tests/port/PortTest.cc @@ -0,0 +1,167 @@ +// Copyright 2026 Matteo Fusi and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define BOOST_TEST_MODULE Port +#include + +#include +#include + +#include "support/BoostInclude.h" + +namespace Simo::Tests { +namespace { + +class NamedPort final : public Simo::Port { + public: + [[nodiscard]] bool connect(Simo::Port* other) override { + return other != nullptr; + } +}; + +} // namespace + +BOOST_AUTO_TEST_CASE(PortNameGetterAndSetter) { + NamedPort left; + NamedPort right; + + left.name("left_port"); + right.name("right_port"); + + BOOST_CHECK_EQUAL(left.name(), "left_port"); + BOOST_CHECK_EQUAL(right.name(), "right_port"); + BOOST_CHECK_EQUAL(left.connect(&right), true); + BOOST_CHECK_EQUAL(left.connect(nullptr), false); +} + +BOOST_AUTO_TEST_CASE(CallbackInPortSendReturnsFalseWhenDisconnected) { + Ports::CallbackOutPort out; + + BOOST_CHECK(!out.send(1)); +} + +BOOST_AUTO_TEST_CASE(CallbackInPortSendReturnsFalseWhenOutPortHasNoCallback) { + Ports::CallbackOutPort out; + Ports::CallbackInPort in; + + BOOST_CHECK_EQUAL(out.connect(&in), true); + BOOST_CHECK_EQUAL(in.has_callback(), false); + BOOST_CHECK(!out.send(1)); +} + +BOOST_AUTO_TEST_CASE(CallbackInPortSendsPayloadToOutPortCallback) { + Ports::CallbackOutPort out; + Ports::CallbackInPort in; + int received_sum = 0; + int callback_count = 0; + + in.callback([&](const int payload) { + received_sum += payload; + ++callback_count; + return true; + }); + + BOOST_CHECK_EQUAL(in.has_callback(), true); + BOOST_CHECK_EQUAL(out.connect(&in), true); + const auto first_result = out.send(3); + BOOST_REQUIRE(first_result); + BOOST_CHECK_EQUAL(*first_result, true); + + constexpr int next_payload = 4; + const auto result = out.send(next_payload); + BOOST_REQUIRE(result); + BOOST_CHECK_EQUAL(*result, true); + + BOOST_CHECK_EQUAL(received_sum, 7); + BOOST_CHECK_EQUAL(callback_count, 2); +} + +BOOST_AUTO_TEST_CASE(CallbackOutPortCanConnectToCallbackInPort) { + Ports::CallbackOutPort out; + Ports::CallbackInPort in( + [](const int /*payload*/) { return true; }); + + BOOST_CHECK_EQUAL(in.connect(&out), true); + auto result = out.send(9); + BOOST_REQUIRE(result); + BOOST_CHECK_EQUAL(*result, true); +} + +BOOST_AUTO_TEST_CASE(CallbackPortsRejectMismatchedPayloadTypes) { + Ports::CallbackOutPort int_out; + Ports::CallbackInPort int_in( + [](const int /*payload*/) { return true; }); + Ports::CallbackOutPort string_in; + Ports::CallbackInPort string_out( + [](const std::string /*payload*/) { return true; }); + + BOOST_CHECK_EQUAL(int_out.connect(&string_out), false); + BOOST_CHECK_EQUAL(string_out.connect(&int_out), false); + BOOST_CHECK_EQUAL(string_in.connect(&int_in), false); + BOOST_CHECK_EQUAL(int_in.connect(&string_in), false); +} + +BOOST_AUTO_TEST_CASE(CallbackPortsRejectNullConnections) { + Ports::CallbackOutPort out; + Ports::CallbackInPort in( + [](const int /*payload*/) { return true; }); + + BOOST_CHECK_EQUAL(out.connect(nullptr), false); + BOOST_CHECK_EQUAL(in.connect(nullptr), false); +} + +BOOST_AUTO_TEST_CASE(CallbackPortsConnectionDifferentPayloadType) { + Ports::CallbackOutPort out; + Ports::CallbackInPort in( + [](const int /*payload*/) { return true; }); + BOOST_CHECK_EQUAL(out.connect(&in), false); +} + +BOOST_AUTO_TEST_CASE(CallbackPortsConnectionDifferentReturnType) { + Ports::CallbackOutPort out; + Ports::CallbackInPort in([](const int /*payload*/) {}); + BOOST_CHECK_EQUAL(out.connect(&in), false); +} + +BOOST_AUTO_TEST_CASE(CallbackPortsConnectionReferencePayload) { + Ports::CallbackOutPort out; + Ports::CallbackInPort in([](int& ref) { ref = 1; }); + BOOST_CHECK_EQUAL(out.connect(&in), true); + int value = 0; + out.send(value); + BOOST_CHECK_EQUAL(value, 1); +} + +BOOST_AUTO_TEST_CASE(CallbackInPortMovesRvaluePayloadToCallback) { + Ports::CallbackOutPort, bool> out; + Ports::CallbackInPort, bool> in; + int received_value = 0; + + in.callback([&](std::unique_ptr payload) { + BOOST_REQUIRE_NE(payload, nullptr); + received_value = *payload; + return true; + }); + + auto payload = std::make_unique(42); + + BOOST_CHECK_EQUAL(out.connect(&in), true); + auto result = out.send(std::move(payload)); + BOOST_REQUIRE(result); + BOOST_CHECK_EQUAL(*result, true); + BOOST_CHECK_EQUAL(payload, nullptr); + BOOST_CHECK_EQUAL(received_value, 42); +} + +} // namespace Simo::Tests diff --git a/tests/statistics/StatisticsTest.cc b/tests/statistics/StatisticsTest.cc index 3736e96..a837cc3 100644 --- a/tests/statistics/StatisticsTest.cc +++ b/tests/statistics/StatisticsTest.cc @@ -29,6 +29,7 @@ #include "support/BoostInclude.h" +namespace Simo::Tests { namespace { class NamedPort final : public Simo::Port { @@ -467,3 +468,4 @@ BOOST_AUTO_TEST_CASE(CollectorInitializeFailsWithInvalidParameters) { BOOST_CHECK_EQUAL(collector.initialize(sim_ctx, params).success(), false); } +} // namespace Simo::Tests