From 35e7539f65e6f800a5221db0d486cd8b9d5b855d Mon Sep 17 00:00:00 2001 From: Ragnar Smestad <82478741+ragnar-rock@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:46:20 +0100 Subject: [PATCH] Initial commit --- .clang-format | 66 ++ .gitignore | 13 + CMakeLists.txt | 123 +++ LICENSE | 21 + README.md | 244 +++++ cmake/NSIS.template.in | 989 ++++++++++++++++++ cmake/cmake-boilerplate.cmake | 139 +++ cmake/config.cmake.in | 53 + cmake/enable-tests.cmake | 17 + cmake/nsis-banner.bmp | Bin 0 -> 25818 bytes cmake/nsis-icon.ico | Bin 0 -> 187844 bytes cmake/nsis-welcome.bmp | Bin 0 -> 154542 bytes cmake/packaging.cmake | 78 ++ cmake/uninstall.cmake | 26 + conanfile.py | 152 +++ core/CMakeLists.txt | 15 + core/core-config.cmake.in | 9 + .../superflow/buffered_consumer_port.h | 241 +++++ .../superflow/callback_consumer_port.h | 123 +++ core/include/superflow/connection_manager.h | 181 ++++ core/include/superflow/connection_spec.h | 15 + core/include/superflow/consumer_port.h | 83 ++ core/include/superflow/factory.h | 17 + core/include/superflow/factory_map.h | 86 ++ core/include/superflow/graph.h | 120 +++ core/include/superflow/graph_factory.h | 53 + core/include/superflow/interface_port.h | 207 ++++ core/include/superflow/mapped_asset_manager.h | 112 ++ core/include/superflow/multi_consumer_port.h | 231 ++++ core/include/superflow/multi_queue_getter.h | 147 +++ core/include/superflow/multi_requester_port.h | 160 +++ core/include/superflow/policy.h | 29 + core/include/superflow/port.h | 42 + core/include/superflow/port_manager.h | 56 + core/include/superflow/port_status.h | 18 + core/include/superflow/producer_port.h | 255 +++++ core/include/superflow/proxel.h | 70 ++ core/include/superflow/proxel_config.h | 16 + core/include/superflow/proxel_status.h | 63 ++ core/include/superflow/queue_getter.h | 65 ++ core/include/superflow/queue_set_getter.h | 82 ++ core/include/superflow/requester_port.h | 201 ++++ core/include/superflow/responder_port.h | 203 ++++ core/include/superflow/utils/blocker.h | 75 ++ core/include/superflow/utils/data_stream.h | 118 +++ core/include/superflow/utils/graphviz.h | 81 ++ core/include/superflow/utils/lock_queue.h | 231 ++++ core/include/superflow/utils/metronome.h | 68 ++ .../superflow/utils/multi_lock_queue.h | 467 +++++++++ core/include/superflow/utils/mutexed.h | 126 +++ core/include/superflow/utils/pimpl_h.h | 73 ++ core/include/superflow/utils/pimpl_impl.h | 29 + core/include/superflow/utils/proxel_timer.h | 80 ++ core/include/superflow/utils/shared_mutexed.h | 133 +++ core/include/superflow/utils/signal_waiter.h | 49 + core/include/superflow/utils/sleeper.h | 48 + .../superflow/utils/terminated_exception.h | 22 + core/include/superflow/utils/throttle.h | 116 ++ .../include/superflow/utils/wait_for_signal.h | 13 + core/include/superflow/value.h | 45 + core/src/graph.cpp | 205 ++++ core/src/graphviz.cpp | 116 ++ core/src/metronome.cpp | 82 ++ core/src/port_manager.cpp | 58 + core/src/proxel.cpp | 49 + core/src/proxel_timer.cpp | 67 ++ core/src/signal_waiter.cpp | 147 +++ core/src/sleeper.cpp | 21 + core/src/wait_for_signal.cpp | 13 + core/test/CMakeLists.txt | 52 + core/test/connectable_port.h | 72 ++ core/test/connectable_proxel.h | 35 + core/test/crashing_proxel.h | 19 + core/test/multi_connectable_port.h | 85 ++ core/test/pimpl_test.cpp | 16 + core/test/pimpl_test.h | 18 + core/test/templated_testproxel.h | 65 ++ core/test/test_block_lock_queue.cpp | 414 ++++++++ core/test/test_buffered_consumer_port.cpp | 587 +++++++++++ core/test/test_callback_consumer_port.cpp | 189 ++++ core/test/test_connection_manager.cpp | 261 +++++ core/test/test_graph.cpp | 364 +++++++ core/test/test_graph_factory.cpp | 139 +++ core/test/test_interface_port.cpp | 107 ++ core/test/test_lock_queue.cpp | 266 +++++ core/test/test_metronome.cpp | 86 ++ core/test/test_multi_consumer_port.cpp | 311 ++++++ core/test/test_multi_lock_queue.cpp | 979 +++++++++++++++++ core/test/test_multi_queue_getter.cpp | 47 + core/test/test_multi_requester_port.cpp | 202 ++++ core/test/test_mutexed.cpp | 392 +++++++ core/test/test_pimpl.cpp | 13 + core/test/test_port_manager.cpp | 90 ++ core/test/test_producer_consumer_port.cpp | 499 +++++++++ core/test/test_proxel_status.cpp | 88 ++ core/test/test_proxel_timer.cpp | 107 ++ core/test/test_requester_responder_port.cpp | 548 ++++++++++ core/test/test_shared_mutexed.cpp | 139 +++ core/test/test_signal_waiter.cpp | 114 ++ core/test/test_sleeper.cpp | 32 + core/test/test_throttle.cpp | 127 +++ core/test/threaded_proxel.h | 52 + curses/CMakeLists.txt | 17 + curses/curses-config.cmake.in | 10 + curses/include/superflow/curses/graph_gui.h | 69 ++ .../include/superflow/curses/proxel_window.h | 50 + curses/include/superflow/curses/window.h | 34 + curses/src/graph_gui.cpp | 278 +++++ curses/src/proxel_window.cpp | 180 ++++ curses/src/window.cpp | 137 +++ curses/test/CMakeLists.txt | 22 + curses/test/test_superflow_curses.cpp | 14 + doc/Doxyfile | 338 ++++++ doc/img/superflow-graph.png | Bin 0 -> 116422 bytes loader/CMakeLists.txt | 24 + loader/README.md | 103 ++ .../include/superflow/loader/load_factories.h | 40 + .../include/superflow/loader/proxel_library.h | 80 ++ .../superflow/loader/register_factory.h | 77 ++ loader/loader-config.cmake.in | 9 + loader/src/proxel_library.cpp | 62 ++ loader/test/CMakeLists.txt | 40 + loader/test/README.md | 55 + loader/test/libs/libdependent_library.so | Bin 0 -> 15936 bytes loader/test/libs/libmissing_dependency.so | Bin 0 -> 16832 bytes loader/test/proxels/CMakeLists.txt | 77 ++ loader/test/proxels/dummy.cpp | 32 + loader/test/proxels/dummy_value_adapter.cpp | 7 + loader/test/proxels/fauxade.h | 14 + .../include/testing/dummy_value_adapter.h | 47 + loader/test/proxels/lib_path.h.in | 10 + loader/test/proxels/mummy.cpp | 36 + loader/test/proxels/yummy.cpp | 37 + loader/test/test_load_factories.cpp | 25 + loader/test/test_proxel_library.cpp | 129 +++ loader/test/test_rtld_now.cpp | 29 + test_package/CMakeLists.txt | 20 + test_package/GenerateTestFromModule.cmake | 16 + test_package/conanfile.py | 28 + .../src/superflowcore_runtime_test.cpp | 7 + .../src/superflowcurses_runtime_test.cpp | 6 + .../src/superflowloader_runtime_test.cpp | 13 + .../src/superflowyaml_runtime_test.cpp | 11 + yaml/CMakeLists.txt | 26 + yaml/include/superflow/yaml/factory.h | 14 + yaml/include/superflow/yaml/yaml.h | 112 ++ .../superflow/yaml/yaml_property_list.h | 65 ++ .../include/superflow/yaml/yaml_string_pair.h | 65 ++ yaml/src/yaml.cpp | 688 ++++++++++++ yaml/test/CMakeLists.txt | 25 + yaml/test/test_yaml_property_list.cpp | 49 + yaml/test/yaml-test-proxel.cpp | 37 + yaml/yaml-config.cmake.in | 9 + 153 files changed, 17141 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/NSIS.template.in create mode 100644 cmake/cmake-boilerplate.cmake create mode 100644 cmake/config.cmake.in create mode 100644 cmake/enable-tests.cmake create mode 100644 cmake/nsis-banner.bmp create mode 100644 cmake/nsis-icon.ico create mode 100644 cmake/nsis-welcome.bmp create mode 100644 cmake/packaging.cmake create mode 100644 cmake/uninstall.cmake create mode 100644 conanfile.py create mode 100644 core/CMakeLists.txt create mode 100644 core/core-config.cmake.in create mode 100644 core/include/superflow/buffered_consumer_port.h create mode 100644 core/include/superflow/callback_consumer_port.h create mode 100644 core/include/superflow/connection_manager.h create mode 100644 core/include/superflow/connection_spec.h create mode 100644 core/include/superflow/consumer_port.h create mode 100644 core/include/superflow/factory.h create mode 100644 core/include/superflow/factory_map.h create mode 100644 core/include/superflow/graph.h create mode 100644 core/include/superflow/graph_factory.h create mode 100644 core/include/superflow/interface_port.h create mode 100644 core/include/superflow/mapped_asset_manager.h create mode 100644 core/include/superflow/multi_consumer_port.h create mode 100644 core/include/superflow/multi_queue_getter.h create mode 100644 core/include/superflow/multi_requester_port.h create mode 100644 core/include/superflow/policy.h create mode 100644 core/include/superflow/port.h create mode 100644 core/include/superflow/port_manager.h create mode 100644 core/include/superflow/port_status.h create mode 100644 core/include/superflow/producer_port.h create mode 100644 core/include/superflow/proxel.h create mode 100644 core/include/superflow/proxel_config.h create mode 100644 core/include/superflow/proxel_status.h create mode 100644 core/include/superflow/queue_getter.h create mode 100644 core/include/superflow/queue_set_getter.h create mode 100644 core/include/superflow/requester_port.h create mode 100644 core/include/superflow/responder_port.h create mode 100644 core/include/superflow/utils/blocker.h create mode 100644 core/include/superflow/utils/data_stream.h create mode 100644 core/include/superflow/utils/graphviz.h create mode 100644 core/include/superflow/utils/lock_queue.h create mode 100644 core/include/superflow/utils/metronome.h create mode 100644 core/include/superflow/utils/multi_lock_queue.h create mode 100644 core/include/superflow/utils/mutexed.h create mode 100644 core/include/superflow/utils/pimpl_h.h create mode 100644 core/include/superflow/utils/pimpl_impl.h create mode 100644 core/include/superflow/utils/proxel_timer.h create mode 100644 core/include/superflow/utils/shared_mutexed.h create mode 100644 core/include/superflow/utils/signal_waiter.h create mode 100644 core/include/superflow/utils/sleeper.h create mode 100644 core/include/superflow/utils/terminated_exception.h create mode 100644 core/include/superflow/utils/throttle.h create mode 100644 core/include/superflow/utils/wait_for_signal.h create mode 100644 core/include/superflow/value.h create mode 100644 core/src/graph.cpp create mode 100644 core/src/graphviz.cpp create mode 100644 core/src/metronome.cpp create mode 100644 core/src/port_manager.cpp create mode 100644 core/src/proxel.cpp create mode 100644 core/src/proxel_timer.cpp create mode 100644 core/src/signal_waiter.cpp create mode 100644 core/src/sleeper.cpp create mode 100644 core/src/wait_for_signal.cpp create mode 100644 core/test/CMakeLists.txt create mode 100644 core/test/connectable_port.h create mode 100644 core/test/connectable_proxel.h create mode 100644 core/test/crashing_proxel.h create mode 100644 core/test/multi_connectable_port.h create mode 100644 core/test/pimpl_test.cpp create mode 100644 core/test/pimpl_test.h create mode 100644 core/test/templated_testproxel.h create mode 100644 core/test/test_block_lock_queue.cpp create mode 100644 core/test/test_buffered_consumer_port.cpp create mode 100644 core/test/test_callback_consumer_port.cpp create mode 100644 core/test/test_connection_manager.cpp create mode 100644 core/test/test_graph.cpp create mode 100644 core/test/test_graph_factory.cpp create mode 100644 core/test/test_interface_port.cpp create mode 100644 core/test/test_lock_queue.cpp create mode 100644 core/test/test_metronome.cpp create mode 100644 core/test/test_multi_consumer_port.cpp create mode 100644 core/test/test_multi_lock_queue.cpp create mode 100644 core/test/test_multi_queue_getter.cpp create mode 100644 core/test/test_multi_requester_port.cpp create mode 100644 core/test/test_mutexed.cpp create mode 100644 core/test/test_pimpl.cpp create mode 100644 core/test/test_port_manager.cpp create mode 100644 core/test/test_producer_consumer_port.cpp create mode 100644 core/test/test_proxel_status.cpp create mode 100644 core/test/test_proxel_timer.cpp create mode 100644 core/test/test_requester_responder_port.cpp create mode 100644 core/test/test_shared_mutexed.cpp create mode 100644 core/test/test_signal_waiter.cpp create mode 100644 core/test/test_sleeper.cpp create mode 100644 core/test/test_throttle.cpp create mode 100644 core/test/threaded_proxel.h create mode 100644 curses/CMakeLists.txt create mode 100644 curses/curses-config.cmake.in create mode 100644 curses/include/superflow/curses/graph_gui.h create mode 100644 curses/include/superflow/curses/proxel_window.h create mode 100644 curses/include/superflow/curses/window.h create mode 100644 curses/src/graph_gui.cpp create mode 100644 curses/src/proxel_window.cpp create mode 100644 curses/src/window.cpp create mode 100644 curses/test/CMakeLists.txt create mode 100644 curses/test/test_superflow_curses.cpp create mode 100644 doc/Doxyfile create mode 100644 doc/img/superflow-graph.png create mode 100644 loader/CMakeLists.txt create mode 100644 loader/README.md create mode 100644 loader/include/superflow/loader/load_factories.h create mode 100644 loader/include/superflow/loader/proxel_library.h create mode 100644 loader/include/superflow/loader/register_factory.h create mode 100644 loader/loader-config.cmake.in create mode 100644 loader/src/proxel_library.cpp create mode 100644 loader/test/CMakeLists.txt create mode 100644 loader/test/README.md create mode 100755 loader/test/libs/libdependent_library.so create mode 100755 loader/test/libs/libmissing_dependency.so create mode 100644 loader/test/proxels/CMakeLists.txt create mode 100644 loader/test/proxels/dummy.cpp create mode 100644 loader/test/proxels/dummy_value_adapter.cpp create mode 100644 loader/test/proxels/fauxade.h create mode 100644 loader/test/proxels/include/testing/dummy_value_adapter.h create mode 100644 loader/test/proxels/lib_path.h.in create mode 100644 loader/test/proxels/mummy.cpp create mode 100644 loader/test/proxels/yummy.cpp create mode 100644 loader/test/test_load_factories.cpp create mode 100644 loader/test/test_proxel_library.cpp create mode 100644 loader/test/test_rtld_now.cpp create mode 100644 test_package/CMakeLists.txt create mode 100644 test_package/GenerateTestFromModule.cmake create mode 100644 test_package/conanfile.py create mode 100644 test_package/src/superflowcore_runtime_test.cpp create mode 100644 test_package/src/superflowcurses_runtime_test.cpp create mode 100644 test_package/src/superflowloader_runtime_test.cpp create mode 100644 test_package/src/superflowyaml_runtime_test.cpp create mode 100644 yaml/CMakeLists.txt create mode 100644 yaml/include/superflow/yaml/factory.h create mode 100644 yaml/include/superflow/yaml/yaml.h create mode 100644 yaml/include/superflow/yaml/yaml_property_list.h create mode 100644 yaml/include/superflow/yaml/yaml_string_pair.h create mode 100644 yaml/src/yaml.cpp create mode 100644 yaml/test/CMakeLists.txt create mode 100644 yaml/test/test_yaml_property_list.cpp create mode 100644 yaml/test/yaml-test-proxel.cpp create mode 100644 yaml/yaml-config.cmake.in diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..fa2f48a --- /dev/null +++ b/.clang-format @@ -0,0 +1,66 @@ +# Generated from CLion C/C++ Code Style settings +BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: None +AlignOperands: Align +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Always +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterUnion: true + BeforeCatch: false + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeColon +ColumnLimit: 0 +CompactNamespaces: false +ContinuationIndentWidth: 2 +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: None +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PointerAlignment: Left +ReflowComments: false +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 0 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 2 +UseTab: Never diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..165a7d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.devcontainer +.idea +.vs +.vscode +*.tag +3rd-party +build +cmake-build* +CMakeSettings.json +CMakeUserPresets.json +html +latex +lib_path.h diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1d61f36 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,123 @@ +cmake_minimum_required(VERSION 3.10.2) +project(superflow VERSION 4.0.1) +message(STATUS "* Generating '${PROJECT_NAME}' v${${PROJECT_NAME}_VERSION}") + +set(CMAKE_DEBUG_POSTFIX "d") +set_property(GLOBAL PROPERTY USE_FOLDERS ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +string(TOLOWER ${CMAKE_PROJECT_NAME} package_name) +set(config_install_dir "share/cmake/${package_name}/") +set(namespace "${package_name}::") +set(project_config_in "${CMAKE_CURRENT_LIST_DIR}/cmake/config.cmake.in") +set(project_config_out "${CMAKE_BINARY_DIR}/${package_name}-config.cmake") +set(targets_export_name "${package_name}-targets") +set(version_config_out "${CMAKE_BINARY_DIR}/${package_name}-config-version.cmake") + +option(BUILD_TESTS "Whether or not to build the tests" OFF) +option(BUILD_all "build superflow with all submodules" ON) +option(BUILD_curses "build submodule curses" OFF) +option(BUILD_loader "build submodule loader" OFF) +option(BUILD_yaml "build submodule yaml" OFF) + +if (BUILD_TESTS) + include(cmake/enable-tests.cmake) +endif() + +include(${CMAKE_BINARY_DIR}/conan_paths.cmake OPTIONAL) +include(${CMAKE_CURRENT_SOURCE_DIR}/build/conan_paths.cmake OPTIONAL) +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_BINARY_DIR}) + +include(CMakePackageConfigHelpers) +include(cmake/cmake-boilerplate.cmake) + +add_subdirectory(core) + +if (NOT MSVC AND (BUILD_curses OR BUILD_all)) + add_subdirectory(curses) +endif() +if (BUILD_loader OR BUILD_all) + add_subdirectory(loader) +endif() +if (BUILD_yaml OR BUILD_all) + add_subdirectory(yaml) +endif() + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +configure_package_config_file( + ${project_config_in} + ${project_config_out} + INSTALL_DESTINATION ${config_install_dir} + PATH_VARS built_components + NO_SET_AND_CHECK_MACRO + ) + +write_basic_package_version_file( + ${version_config_out} + COMPATIBILITY SameMajorVersion + ) + +install(FILES + ${project_config_out} + ${version_config_out} + DESTINATION ${config_install_dir} + COMPONENT core + ) + +install(FILES + ${CMAKE_SOURCE_DIR}/LICENSE + DESTINATION "${CMAKE_INSTALL_DOCDIR}" + COMPONENT core + ) + +install(EXPORT ${targets_export_name} + NAMESPACE ${namespace} + DESTINATION ${config_install_dir} + COMPONENT core + ) + +export( + TARGETS ${built_components} + NAMESPACE ${namespace} + FILE ${CMAKE_BINARY_DIR}/${targets_export_name}.cmake + ) + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Create target 'doxygen' +find_package(Doxygen) +if (Doxygen_FOUND) + add_custom_target(doxygen + COMMAND + ${PROJECT_NAME}_VERSION=v${${PROJECT_NAME}_VERSION} + ${DOXYGEN_EXECUTABLE} + ${CMAKE_CURRENT_LIST_DIR}/doc/Doxyfile + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + COMMENT "Generating API documentation with Doxygen" + VERBATIM + ) +endif() + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Create target 'pack' +get_cmake_property(CPACK_COMPONENTS_ALL COMPONENTS) +include(cmake/packaging.cmake) + +add_custom_target(pack + COMMAND + ${CMAKE_CPACK_COMMAND} "-G" "DEB" + "-D" "CPACK_COMPONENTS_GROUPING=ALL_COMPONENTS_IN_ONE" + # Denne virker ikke. Alle pakkene blir "installert" likevel. + #COMMAND + # ${CMAKE_CPACK_COMMAND} "-G" "DEB" + # "-D" "CPACK_COMPONENTS_GROUPING=IGNORE" + COMMENT "Running CPack. Please wait..." + DEPENDS ${CPACK_COMPONENTS_ALL} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Create target 'uninstall' +add_custom_target(uninstall + "${CMAKE_COMMAND}" -P "${CMAKE_CURRENT_LIST_DIR}/cmake/uninstall.cmake" +) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94eaaf6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Norwegian Defence Research Establishment (FFI) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e3fde1 --- /dev/null +++ b/README.md @@ -0,0 +1,244 @@ +# Superflow - An efficient processing framework for modern C++ + +Authors: Ragnar Smestad, Martin Vonheim Larsen, Trym Vegard Haavardsholm. + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +  +[![Latest release](https://img.shields.io/github/v/release/FFI-no/superflow?label=Latest%20Release)]() + + +## Contents + +* [What is this?](#what-is-superflow) +* [When should I use this?](#when-should-i-use-superflow) +* [Getting started](#getting-started) + * [Modules](#modules) + * [Requirements](#requirements) + * [Install](#install) + * [Usage](#usage) + * [Testing](#testing) +* [Contribute](#contribute) +* [License](#license) +* [Citing](#citing-this-code) + + +## What is Superflow? + +Superflow is made for creating and running flexible processing graphs, where the nodes are the individual processing stages, and the edges are data flows between nodes. +Processing stages are represented by concurrent _proxels_, that encapsulate self-contained parts of the processing system, such as specific algorithms, file or device i/o, or even parts of a graphical user interface. +The data flows are represented by connected _ports_, that together implement a chosen multi-process communication scheme. +A proxel typically has input ports for receiving or requesting data, and output ports for providing results. +The proxels and ports are managed in a container class called _Graph_, which offers a convenient way to start and stop the processing graph, add and connect proxels, monitor the status of the processing graph, and more. +In order to simplify the creation of a graph and to be flexible to changes in content and structure, Superflow provides tools for parsing configuration files containing lists of proxels, parameters and connections. +This can be used to create and start graphs automatically without the need to recompile any code. + +The design of Superflow makes it simple to combine different sensors, algorithms and processing stages, and to dynamically reconfigure established processing pipelines. +The framework supports parallel processing, branching and merging of pipelines, and synchronization through barriers and latches. +This is all performed in an efficient, type safe and extendible communication scheme based on modern C++. + +A thorough introduction to Superflow is written in [FFI report 24/00556]. +The report contains a description of the main components in Superflow, followed by a short tutorial that will get you started on using Superflow in your own applications. + + + +## When should I use Superflow? + +In Superflow, data flow and data processing are decoupled and focused through _port_ and _proxel_ abstractions. +This makes it simple to combine different sensors, algorithms and processing stages, and to dynamically reconfigure established processing pipelines. +There are other methods and other libraries available for data processing, which also offer low coupling between the software elements in the pipeline. + +In our opinion, however, the main reasons to select Superflow are: + + - [x] high level of flexibility and efficiency + - [x] few additional requirements (the `core` module has no dependencies) + - [x] strongly typed, non-serialized, possibly zero-copy data transfer between proxels through ports + - [x] no custom build tool or bloated ecosystem, just pure modern CMake + +Although originally developed for real-time processing on autonomous vehicles, we are certain that Superflow will be useful in a multitude of other interesting applications as well. + +## Getting started + +### Modules + +In this repository, Superflow comes with the following modules: +- core +- curses +- loader +- yaml + +#### core + +contains all the required components in order to create proxels and use ports. It does not depend on any external libraries. + +#### curses + +offers a simple GUI for terminal, based on _ncurses_. It is thus not available for Windows. + +See also the [curses/README]. + +Dependencies: +- ncurses (`libncurses-dev`) +- [Ncursescpp] + +#### loader + +enables dynamic loading of proxel libraries (shared libraries). +A `loader`-compatible library has embedded a list of which proxels it can provide, +so that the user does not have to include any specific header files or hard code proxel names in their client code. +The functionality is provided by [Boost.DLL]. + +See also the [loader/README]. + +Dependencies: +- Boost (`libboost-dev`) +- Boost.Filesystem (`libboost-filesystem-dev`) + +#### yaml + +enables creation and customization of a processing graph based on YAML formatted configuration files. + +Dependencies: +- yaml-cpp (`libyaml-cpp-dev`) + +### Requirements + +The following installation guide is aimed at Ubuntu Linux. + +You will need +- Compiler with support for C++17 +- CMake > 3.10.2 +- Boost.DLL (for the `loader` module) +- ncurses and ncursescpp (for the `curses` module) +- yaml-cpp (for the `yaml` module) + +```bash +sudo apt update +sudo apt install -y \ + build-essential \ + cmake \ + libboost-filesystem-dev \ + libncurses-dev \ + libyaml-cpp-dev +``` + +See [`ncursescpp`](https://github.com/solosuper/Ncursescpp) for their installation instructions. + +### Install + +Configure, build and install with CMake. + +A typical default setup can be installed like so: +(`sudo` because it is usually required with the default install prefix). + +```bash +git clone https://github.com/ffi-no/superflow +cd superflow +cmake -B build # configure +cmake --build build # build +[sudo] cmake --build build --target install # install +``` + +The configuration step can be customized with additional flags to CMake. +An overview of standard parameter values is given in the table: + +| Parameter | Default | +|:-------------------|:-------:| +| BUILD_all | ON | +| BUILD_curses | OFF | +| BUILD_loader | OFF | +| BUILD_yaml | OFF | +| BUILD_SHARED_LIBS | OFF | +| BUILD_TESTS | OFF | + +The `core` module is always on. +If you e.g. want to turn off `curses`, but keep `loader` and `yaml`, +adapt the configure step: + +```bash +cmake -B build -DBUILD_all=OFF -DBUILD_curses=OFF -DBUILD_loader=ON -DBUILD_yaml=ON +``` + +### Usage +Add Superflow to your CMakeLists.txt + +```cmake +find_package(superflow REQUIRED core curses loader yaml) +target_link_libraries(${PROJECT_NAME} + PUBLIC + superflow::core + superflow::curses + superflow::loader + superflow::yaml + ) +``` + +### Testing + +You can run the superflow tests like this: + +```bash +cmake -B build -DBUILD_TESTS=ON +cmake --build build --target test +``` + +### Packaging + +Superflow has experimental support for packaging. Build the `pack` target in order to create a redistributable Debian package. + +```bash +cmake --build build --target pack +# or +cpack -G DEB +# CPack: - package: /(...)/superflow-dev_4.0.0_amd64.deb generated. +``` + +## Documentation +Documentation can be generated with [doxygen](http://www.doxygen.nl/): + +```bash +[sudo] apt install doxygen graphviz + +cmake --build build --target doxygen +# or +doxygen ./doc/Doxyfile + +# open doc/html/index.html +``` + +## Contribute + +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. +Please make sure to update tests as appropriate. + +You are welcome to post issues with bug reports, but due to constrained resources, we are unfortunately not able to accommodate pure feature requests. + +## License + +The source code is licensed under the [MIT License](https://opensource.org/license/mit/). + +## Citing this code + +If you find Superflow useful in your work, please cite the [FFI report 24/00556] as + +```bibtex +@article{24/00556, + author = {Ragnar Smestad and Martin Vonheim Larsen and Trym Vegard Haavardsholm}, + title = {Superflow - An efficient processing framework for modern C++}, + journal = {Forsvarets Forskningsinstitutt}, + year = {2024}, + month = {feb}, + note = {{FFI}-Rapport 24/00556}, + keywords = {Autonomi, C++ / ProgrammeringssprÃ¥k, Rammeverk, Systemarkitektur}, + FFI-dokument = {{FFI}-Rapport}, + FFI-nummer = {24/00556}, + FFI-gradering = {Ugradert} +} +``` + +--- + +[Boost.DLL]: https://www.boost.org/doc/libs/1_65_0/doc/html/boost_dll.html +[curses/README]: curses/README.md +[loader/README]: loader/README.md +[Ncursescpp]: https://github.com/solosuper/Ncursescpp +[FFI report 24/00556]: https://www.ffi.no/publikasjoner/ffi-rapporter \ No newline at end of file diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in new file mode 100644 index 0000000..1a4ecf6 --- /dev/null +++ b/cmake/NSIS.template.in @@ -0,0 +1,989 @@ +; CPack install script designed for a nmake build + +;-------------------------------- +; You must define these values + + !define VERSION "@CPACK_PACKAGE_VERSION@" + !define PATCH "@CPACK_PACKAGE_VERSION_PATCH@" + !define INST_DIR "@CPACK_TEMPORARY_DIRECTORY@" + +;-------------------------------- +;Variables + + Var MUI_TEMP + Var STARTMENU_FOLDER + Var SV_ALLUSERS + Var START_MENU + Var DO_NOT_ADD_TO_PATH + Var ADD_TO_PATH_ALL_USERS + Var ADD_TO_PATH_CURRENT_USER + Var INSTALL_DESKTOP + Var IS_DEFAULT_INSTALLDIR +;-------------------------------- +;Include Modern UI + + !include "MUI.nsh" + + ;Default installation folder + InstallDir "@CPACK_NSIS_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_DIRECTORY@" + +;-------------------------------- +;General + + ;Name and file + Name "@CPACK_NSIS_PACKAGE_NAME@" + OutFile "@CPACK_TOPLEVEL_DIRECTORY@/@CPACK_OUTPUT_FILE_NAME@" + + ;Set compression + SetCompressor @CPACK_NSIS_COMPRESSOR@ + + ;Require administrator access + ;RequestExecutionLevel admin + RequestExecutionLevel user + +@CPACK_NSIS_DEFINES@ + + !include Sections.nsh + +;--- Component support macros: --- +; The code for the add/remove functionality is from: +; http://nsis.sourceforge.net/Add/Remove_Functionality +; It has been modified slightly and extended to provide +; inter-component dependencies. +Var AR_SecFlags +Var AR_RegFlags +@CPACK_NSIS_SECTION_SELECTED_VARS@ + +; Loads the "selected" flag for the section named SecName into the +; variable VarName. +!macro LoadSectionSelectedIntoVar SecName VarName + SectionGetFlags ${${SecName}} $${VarName} + IntOp $${VarName} $${VarName} & ${SF_SELECTED} ;Turn off all other bits +!macroend + +; Loads the value of a variable... can we get around this? +!macro LoadVar VarName + IntOp $R0 0 + $${VarName} +!macroend + +; Sets the value of a variable +!macro StoreVar VarName IntValue + IntOp $${VarName} 0 + ${IntValue} +!macroend + +!macro InitSection SecName + ; This macro reads component installed flag from the registry and + ;changes checked state of the section on the components page. + ;Input: section index constant name specified in Section command. + + ClearErrors + ;Reading component status from registry + ReadRegDWORD $AR_RegFlags HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\Components\${SecName}" "Installed" + IfErrors "default_${SecName}" + ;Status will stay default if registry value not found + ;(component was never installed) + IntOp $AR_RegFlags $AR_RegFlags & ${SF_SELECTED} ;Turn off all other bits + SectionGetFlags ${${SecName}} $AR_SecFlags ;Reading default section flags + IntOp $AR_SecFlags $AR_SecFlags & 0xFFFE ;Turn lowest (enabled) bit off + IntOp $AR_SecFlags $AR_RegFlags | $AR_SecFlags ;Change lowest bit + + ; Note whether this component was installed before + !insertmacro StoreVar ${SecName}_was_installed $AR_RegFlags + IntOp $R0 $AR_RegFlags & $AR_RegFlags + + ;Writing modified flags + SectionSetFlags ${${SecName}} $AR_SecFlags + + "default_${SecName}:" + !insertmacro LoadSectionSelectedIntoVar ${SecName} ${SecName}_selected +!macroend + +!macro FinishSection SecName + ; This macro reads section flag set by user and removes the section + ;if it is not selected. + ;Then it writes component installed flag to registry + ;Input: section index constant name specified in Section command. + + SectionGetFlags ${${SecName}} $AR_SecFlags ;Reading section flags + ;Checking lowest bit: + IntOp $AR_SecFlags $AR_SecFlags & ${SF_SELECTED} + IntCmp $AR_SecFlags 1 "leave_${SecName}" + ;Section is not selected: + ;Calling Section uninstall macro and writing zero installed flag + !insertmacro "Remove_${${SecName}}" + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\Components\${SecName}" \ + "Installed" 0 + Goto "exit_${SecName}" + + "leave_${SecName}:" + ;Section is selected: + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\Components\${SecName}" \ + "Installed" 1 + + "exit_${SecName}:" +!macroend + +!macro RemoveSection_CPack SecName + ; This macro is used to call section's Remove_... macro + ;from the uninstaller. + ;Input: section index constant name specified in Section command. + + !insertmacro "Remove_${${SecName}}" +!macroend + +; Determine whether the selection of SecName changed +!macro MaybeSelectionChanged SecName + !insertmacro LoadVar ${SecName}_selected + SectionGetFlags ${${SecName}} $R1 + IntOp $R1 $R1 & ${SF_SELECTED} ;Turn off all other bits + + ; See if the status has changed: + IntCmp $R0 $R1 "${SecName}_unchanged" + !insertmacro LoadSectionSelectedIntoVar ${SecName} ${SecName}_selected + + IntCmp $R1 ${SF_SELECTED} "${SecName}_was_selected" + !insertmacro "Deselect_required_by_${SecName}" + goto "${SecName}_unchanged" + + "${SecName}_was_selected:" + !insertmacro "Select_${SecName}_depends" + + "${SecName}_unchanged:" +!macroend +;--- End of Add/Remove macros --- + +;-------------------------------- +;Interface Settings + + !define MUI_HEADERIMAGE + !define MUI_ABORTWARNING + +;---------------------------------------- +; based upon a script of "Written by KiCHiK 2003-01-18 05:57:02" +;---------------------------------------- +!verbose 3 +!include "WinMessages.NSH" +!verbose 4 +;==================================================== +; get_NT_environment +; Returns: the selected environment +; Output : head of the stack +;==================================================== +!macro select_NT_profile UN +Function ${UN}select_NT_profile + StrCmp $ADD_TO_PATH_ALL_USERS "1" 0 environment_single + DetailPrint "Selected environment for all users" + Push "all" + Return + environment_single: + DetailPrint "Selected environment for current user only." + Push "current" + Return +FunctionEnd +!macroend +!insertmacro select_NT_profile "" +!insertmacro select_NT_profile "un." +;---------------------------------------------------- +!define NT_current_env 'HKCU "Environment"' +!define NT_all_env 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"' + +!ifndef WriteEnvStr_RegKey + !ifdef ALL_USERS + !define WriteEnvStr_RegKey \ + 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"' + !else + !define WriteEnvStr_RegKey 'HKCU "Environment"' + !endif +!endif + +; AddToPath - Adds the given dir to the search path. +; Input - head of the stack +; Note - Win9x systems requires reboot + +Function AddToPath + Exch $0 + Push $1 + Push $2 + Push $3 + + # don't add if the path doesn't exist + IfFileExists "$0\*.*" "" AddToPath_done + + ReadEnvStr $1 PATH + ; if the path is too long for a NSIS variable NSIS will return a 0 + ; length string. If we find that, then warn and skip any path + ; modification as it will trash the existing path. + StrLen $2 $1 + IntCmp $2 0 CheckPathLength_ShowPathWarning CheckPathLength_Done CheckPathLength_Done + CheckPathLength_ShowPathWarning: + Messagebox MB_OK|MB_ICONEXCLAMATION "Warning! PATH too long installer unable to modify PATH!" + Goto AddToPath_done + CheckPathLength_Done: + Push "$1;" + Push "$0;" + Call StrStr + Pop $2 + StrCmp $2 "" "" AddToPath_done + Push "$1;" + Push "$0\;" + Call StrStr + Pop $2 + StrCmp $2 "" "" AddToPath_done + GetFullPathName /SHORT $3 $0 + Push "$1;" + Push "$3;" + Call StrStr + Pop $2 + StrCmp $2 "" "" AddToPath_done + Push "$1;" + Push "$3\;" + Call StrStr + Pop $2 + StrCmp $2 "" "" AddToPath_done + + Call IsNT + Pop $1 + StrCmp $1 1 AddToPath_NT + ; Not on NT + StrCpy $1 $WINDIR 2 + FileOpen $1 "$1\autoexec.bat" a + FileSeek $1 -1 END + FileReadByte $1 $2 + IntCmp $2 26 0 +2 +2 # DOS EOF + FileSeek $1 -1 END # write over EOF + FileWrite $1 "$\r$\nSET PATH=%PATH%;$3$\r$\n" + FileClose $1 + SetRebootFlag true + Goto AddToPath_done + + AddToPath_NT: + StrCmp $ADD_TO_PATH_ALL_USERS "1" ReadAllKey + ReadRegStr $1 ${NT_current_env} "PATH" + Goto DoTrim + ReadAllKey: + ReadRegStr $1 ${NT_all_env} "PATH" + DoTrim: + StrCmp $1 "" AddToPath_NTdoIt + Push $1 + Call Trim + Pop $1 + StrCpy $0 "$1;$0" + AddToPath_NTdoIt: + StrCmp $ADD_TO_PATH_ALL_USERS "1" WriteAllKey + WriteRegExpandStr ${NT_current_env} "PATH" $0 + Goto DoSend + WriteAllKey: + WriteRegExpandStr ${NT_all_env} "PATH" $0 + DoSend: + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 + + AddToPath_done: + Pop $3 + Pop $2 + Pop $1 + Pop $0 +FunctionEnd + + +; RemoveFromPath - Remove a given dir from the path +; Input: head of the stack + +Function un.RemoveFromPath + Exch $0 + Push $1 + Push $2 + Push $3 + Push $4 + Push $5 + Push $6 + + IntFmt $6 "%c" 26 # DOS EOF + + Call un.IsNT + Pop $1 + StrCmp $1 1 unRemoveFromPath_NT + ; Not on NT + StrCpy $1 $WINDIR 2 + FileOpen $1 "$1\autoexec.bat" r + GetTempFileName $4 + FileOpen $2 $4 w + GetFullPathName /SHORT $0 $0 + StrCpy $0 "SET PATH=%PATH%;$0" + Goto unRemoveFromPath_dosLoop + + unRemoveFromPath_dosLoop: + FileRead $1 $3 + StrCpy $5 $3 1 -1 # read last char + StrCmp $5 $6 0 +2 # if DOS EOF + StrCpy $3 $3 -1 # remove DOS EOF so we can compare + StrCmp $3 "$0$\r$\n" unRemoveFromPath_dosLoopRemoveLine + StrCmp $3 "$0$\n" unRemoveFromPath_dosLoopRemoveLine + StrCmp $3 "$0" unRemoveFromPath_dosLoopRemoveLine + StrCmp $3 "" unRemoveFromPath_dosLoopEnd + FileWrite $2 $3 + Goto unRemoveFromPath_dosLoop + unRemoveFromPath_dosLoopRemoveLine: + SetRebootFlag true + Goto unRemoveFromPath_dosLoop + + unRemoveFromPath_dosLoopEnd: + FileClose $2 + FileClose $1 + StrCpy $1 $WINDIR 2 + Delete "$1\autoexec.bat" + CopyFiles /SILENT $4 "$1\autoexec.bat" + Delete $4 + Goto unRemoveFromPath_done + + unRemoveFromPath_NT: + StrCmp $ADD_TO_PATH_ALL_USERS "1" unReadAllKey + ReadRegStr $1 ${NT_current_env} "PATH" + Goto unDoTrim + unReadAllKey: + ReadRegStr $1 ${NT_all_env} "PATH" + unDoTrim: + StrCpy $5 $1 1 -1 # copy last char + StrCmp $5 ";" +2 # if last char != ; + StrCpy $1 "$1;" # append ; + Push $1 + Push "$0;" + Call un.StrStr ; Find `$0;` in $1 + Pop $2 ; pos of our dir + StrCmp $2 "" unRemoveFromPath_done + ; else, it is in path + # $0 - path to add + # $1 - path var + StrLen $3 "$0;" + StrLen $4 $2 + StrCpy $5 $1 -$4 # $5 is now the part before the path to remove + StrCpy $6 $2 "" $3 # $6 is now the part after the path to remove + StrCpy $3 $5$6 + + StrCpy $5 $3 1 -1 # copy last char + StrCmp $5 ";" 0 +2 # if last char == ; + StrCpy $3 $3 -1 # remove last char + + StrCmp $ADD_TO_PATH_ALL_USERS "1" unWriteAllKey + WriteRegExpandStr ${NT_current_env} "PATH" $3 + Goto unDoSend + unWriteAllKey: + WriteRegExpandStr ${NT_all_env} "PATH" $3 + unDoSend: + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 + + unRemoveFromPath_done: + Pop $6 + Pop $5 + Pop $4 + Pop $3 + Pop $2 + Pop $1 + Pop $0 +FunctionEnd + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Uninstall sutff +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +########################################### +# Utility Functions # +########################################### + +;==================================================== +; IsNT - Returns 1 if the current system is NT, 0 +; otherwise. +; Output: head of the stack +;==================================================== +; IsNT +; no input +; output, top of the stack = 1 if NT or 0 if not +; +; Usage: +; Call IsNT +; Pop $R0 +; ($R0 at this point is 1 or 0) + +!macro IsNT un +Function ${un}IsNT + Push $0 + ReadRegStr $0 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion" CurrentVersion + StrCmp $0 "" 0 IsNT_yes + ; we are not NT. + Pop $0 + Push 0 + Return + + IsNT_yes: + ; NT!!! + Pop $0 + Push 1 +FunctionEnd +!macroend +!insertmacro IsNT "" +!insertmacro IsNT "un." + +; StrStr +; input, top of stack = string to search for +; top of stack-1 = string to search in +; output, top of stack (replaces with the portion of the string remaining) +; modifies no other variables. +; +; Usage: +; Push "this is a long ass string" +; Push "ass" +; Call StrStr +; Pop $R0 +; ($R0 at this point is "ass string") + +!macro StrStr un +Function ${un}StrStr +Exch $R1 ; st=haystack,old$R1, $R1=needle + Exch ; st=old$R1,haystack + Exch $R2 ; st=old$R1,old$R2, $R2=haystack + Push $R3 + Push $R4 + Push $R5 + StrLen $R3 $R1 + StrCpy $R4 0 + ; $R1=needle + ; $R2=haystack + ; $R3=len(needle) + ; $R4=cnt + ; $R5=tmp + loop: + StrCpy $R5 $R2 $R3 $R4 + StrCmp $R5 $R1 done + StrCmp $R5 "" done + IntOp $R4 $R4 + 1 + Goto loop +done: + StrCpy $R1 $R2 "" $R4 + Pop $R5 + Pop $R4 + Pop $R3 + Pop $R2 + Exch $R1 +FunctionEnd +!macroend +!insertmacro StrStr "" +!insertmacro StrStr "un." + +Function Trim ; Added by Pelaca + Exch $R1 + Push $R2 +Loop: + StrCpy $R2 "$R1" 1 -1 + StrCmp "$R2" " " RTrim + StrCmp "$R2" "$\n" RTrim + StrCmp "$R2" "$\r" RTrim + StrCmp "$R2" ";" RTrim + GoTo Done +RTrim: + StrCpy $R1 "$R1" -1 + Goto Loop +Done: + Pop $R2 + Exch $R1 +FunctionEnd + +Function ConditionalAddToRegisty + Pop $0 + Pop $1 + StrCmp "$0" "" ConditionalAddToRegisty_EmptyString + WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" \ + "$1" "$0" + ;MessageBox MB_OK "Set Registry: '$1' to '$0'" + DetailPrint "Set install registry entry: '$1' to '$0'" + ConditionalAddToRegisty_EmptyString: +FunctionEnd + +;-------------------------------- + +!ifdef CPACK_USES_DOWNLOAD +Function DownloadFile + IfFileExists $INSTDIR\* +2 + CreateDirectory $INSTDIR + Pop $0 + + ; Skip if already downloaded + IfFileExists $INSTDIR\$0 0 +2 + Return + + StrCpy $1 "@CPACK_DOWNLOAD_SITE@" + + try_again: + NSISdl::download "$1/$0" "$INSTDIR\$0" + + Pop $1 + StrCmp $1 "success" success + StrCmp $1 "Cancelled" cancel + MessageBox MB_OK "Download failed: $1" + cancel: + Return + success: +FunctionEnd +!endif + +;-------------------------------- +; Installation types +@CPACK_NSIS_INSTALLATION_TYPES@ + +;-------------------------------- +; Component sections +@CPACK_NSIS_COMPONENT_SECTIONS@ + +;-------------------------------- +; Define some macro setting for the gui +@CPACK_NSIS_INSTALLER_MUI_ICON_CODE@ +@CPACK_NSIS_INSTALLER_ICON_CODE@ +@CPACK_NSIS_INSTALLER_MUI_WELCOMEFINISH_CODE@ +@CPACK_NSIS_INSTALLER_MUI_UNWELCOMEFINISH_CODE@ +@CPACK_NSIS_INSTALLER_MUI_COMPONENTS_DESC@ +@CPACK_NSIS_INSTALLER_MUI_FINISHPAGE_RUN_CODE@ + +!define MUI_HEADERIMAGE_BITMAP "@CPACK_NSIS_MUI_HEADERIMAGE_BITMAP@" + +!define MUI_WELCOMEPAGE_TEXT "\ +Superflow is an efficient processing framework for modern C++, developed at FFI (Norwegian Defence Research Establishment).\r\n\r\n\ +This Setup will guide you through the installation of the superflow library.\r\n\r\n\ +It is recommended that you close all other applications before starting Setup. \ +This will make it possible to update relevant system files without having to reboot your computer.\r\n\r\n\ +Click Next to continue." + +!define MUI_FINISHPAGE_TEXT \ +"Congratulations! Superflow has been installed on your computer.\r\n\r\n\ +For easy integration with CMake, it is recommended to create an environment variable 'CMAKE_PREFIX_PATH' that contains the path to the installation prefix (default '@CPACK_NSIS_INSTALL_ROOT@'). \ +You will have to create it manually.\r\n\r\n\ +To uninstall superflow, run '$INSTDIR\\Uninstall.exe'.\r\n\r\n\ +Click Finish to close Setup." + +;-------------------------------- +;Pages + !insertmacro MUI_PAGE_WELCOME + + !insertmacro MUI_PAGE_LICENSE "@CPACK_RESOURCE_FILE_LICENSE@" + Page custom InstallOptionsPage + !insertmacro MUI_PAGE_DIRECTORY + + ;Start Menu Folder Page Configuration + !define MUI_STARTMENUPAGE_REGISTRY_ROOT "SHCTX" + !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" + !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" + ;!insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER + + @CPACK_NSIS_PAGE_COMPONENTS@ + + !insertmacro MUI_PAGE_INSTFILES + !insertmacro MUI_PAGE_FINISH + + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + +;-------------------------------- +;Languages + + !insertmacro MUI_LANGUAGE "English" ;first language is the default language + !insertmacro MUI_LANGUAGE "Albanian" + !insertmacro MUI_LANGUAGE "Arabic" + !insertmacro MUI_LANGUAGE "Basque" + !insertmacro MUI_LANGUAGE "Belarusian" + !insertmacro MUI_LANGUAGE "Bosnian" + !insertmacro MUI_LANGUAGE "Breton" + !insertmacro MUI_LANGUAGE "Bulgarian" + !insertmacro MUI_LANGUAGE "Croatian" + !insertmacro MUI_LANGUAGE "Czech" + !insertmacro MUI_LANGUAGE "Danish" + !insertmacro MUI_LANGUAGE "Dutch" + !insertmacro MUI_LANGUAGE "Estonian" + !insertmacro MUI_LANGUAGE "Farsi" + !insertmacro MUI_LANGUAGE "Finnish" + !insertmacro MUI_LANGUAGE "French" + !insertmacro MUI_LANGUAGE "German" + !insertmacro MUI_LANGUAGE "Greek" + !insertmacro MUI_LANGUAGE "Hebrew" + !insertmacro MUI_LANGUAGE "Hungarian" + !insertmacro MUI_LANGUAGE "Icelandic" + !insertmacro MUI_LANGUAGE "Indonesian" + !insertmacro MUI_LANGUAGE "Irish" + !insertmacro MUI_LANGUAGE "Italian" + !insertmacro MUI_LANGUAGE "Japanese" + !insertmacro MUI_LANGUAGE "Korean" + !insertmacro MUI_LANGUAGE "Kurdish" + !insertmacro MUI_LANGUAGE "Latvian" + !insertmacro MUI_LANGUAGE "Lithuanian" + !insertmacro MUI_LANGUAGE "Luxembourgish" + !insertmacro MUI_LANGUAGE "Macedonian" + !insertmacro MUI_LANGUAGE "Malay" + !insertmacro MUI_LANGUAGE "Mongolian" + !insertmacro MUI_LANGUAGE "Norwegian" + !insertmacro MUI_LANGUAGE "Polish" + !insertmacro MUI_LANGUAGE "Portuguese" + !insertmacro MUI_LANGUAGE "PortugueseBR" + !insertmacro MUI_LANGUAGE "Romanian" + !insertmacro MUI_LANGUAGE "Russian" + !insertmacro MUI_LANGUAGE "Serbian" + !insertmacro MUI_LANGUAGE "SerbianLatin" + !insertmacro MUI_LANGUAGE "SimpChinese" + !insertmacro MUI_LANGUAGE "Slovak" + !insertmacro MUI_LANGUAGE "Slovenian" + !insertmacro MUI_LANGUAGE "Spanish" + !insertmacro MUI_LANGUAGE "Swedish" + !insertmacro MUI_LANGUAGE "Thai" + !insertmacro MUI_LANGUAGE "TradChinese" + !insertmacro MUI_LANGUAGE "Turkish" + !insertmacro MUI_LANGUAGE "Ukrainian" + !insertmacro MUI_LANGUAGE "Welsh" + + +;-------------------------------- +;Reserve Files + + ;These files should be inserted before other files in the data block + ;Keep these lines before any File command + ;Only for solid compression (by default, solid compression is enabled for BZIP2 and LZMA) + + ReserveFile "NSIS.InstallOptions.ini" + !insertmacro MUI_RESERVEFILE_INSTALLOPTIONS + +;-------------------------------- +;Installer Sections + +Section "-Core installation" + ;Use the entire tree produced by the INSTALL target. Keep the + ;list of directories here in sync with the RMDir commands below. + SetOutPath "$INSTDIR" + @CPACK_NSIS_EXTRA_PREINSTALL_COMMANDS@ + @CPACK_NSIS_FULL_INSTALL@ + + ;Store installation folder + WriteRegStr SHCTX "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "" $INSTDIR + + ;Create uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + Push "DisplayName" + Push "@CPACK_NSIS_DISPLAY_NAME@" + Call ConditionalAddToRegisty + Push "DisplayVersion" + Push "@CPACK_PACKAGE_VERSION@" + Call ConditionalAddToRegisty + Push "Publisher" + Push "@CPACK_PACKAGE_VENDOR@" + Call ConditionalAddToRegisty + Push "UninstallString" + Push "$INSTDIR\Uninstall.exe" + Call ConditionalAddToRegisty + Push "NoRepair" + Push "1" + Call ConditionalAddToRegisty + + !ifdef CPACK_NSIS_ADD_REMOVE + ;Create add/remove functionality + Push "ModifyPath" + Push "$INSTDIR\AddRemove.exe" + Call ConditionalAddToRegisty + !else + Push "NoModify" + Push "1" + Call ConditionalAddToRegisty + !endif + + ; Optional registration + Push "DisplayIcon" + Push "$INSTDIR\@CPACK_NSIS_INSTALLED_ICON_NAME@" + Call ConditionalAddToRegisty + Push "HelpLink" + Push "@CPACK_NSIS_HELP_LINK@" + Call ConditionalAddToRegisty + Push "URLInfoAbout" + Push "@CPACK_NSIS_URL_INFO_ABOUT@" + Call ConditionalAddToRegisty + Push "Contact" + Push "@CPACK_NSIS_CONTACT@" + Call ConditionalAddToRegisty + !insertmacro MUI_INSTALLOPTIONS_READ $INSTALL_DESKTOP "NSIS.InstallOptions.ini" "Field 5" "State" + ;!insertmacro MUI_STARTMENU_WRITE_BEGIN Application + + ;Create shortcuts + CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" +@CPACK_NSIS_CREATE_ICONS@ +@CPACK_NSIS_CREATE_ICONS_EXTRA@ + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "$INSTDIR\Uninstall.exe" + + ;Read a value from an InstallOptions INI file + !insertmacro MUI_INSTALLOPTIONS_READ $DO_NOT_ADD_TO_PATH "NSIS.InstallOptions.ini" "Field 2" "State" + !insertmacro MUI_INSTALLOPTIONS_READ $ADD_TO_PATH_ALL_USERS "NSIS.InstallOptions.ini" "Field 3" "State" + !insertmacro MUI_INSTALLOPTIONS_READ $ADD_TO_PATH_CURRENT_USER "NSIS.InstallOptions.ini" "Field 4" "State" + + ; Write special uninstall registry entries + Push "StartMenu" + Push "$STARTMENU_FOLDER" + Call ConditionalAddToRegisty + Push "DoNotAddToPath" + Push "$DO_NOT_ADD_TO_PATH" + Call ConditionalAddToRegisty + Push "AddToPathAllUsers" + Push "$ADD_TO_PATH_ALL_USERS" + Call ConditionalAddToRegisty + Push "AddToPathCurrentUser" + Push "$ADD_TO_PATH_CURRENT_USER" + Call ConditionalAddToRegisty + Push "InstallToDesktop" + Push "$INSTALL_DESKTOP" + Call ConditionalAddToRegisty + + !insertmacro MUI_STARTMENU_WRITE_END + +@CPACK_NSIS_EXTRA_INSTALL_COMMANDS@ + +SectionEnd + +Section "-Add to path" + Push $INSTDIR\bin + StrCmp "@CPACK_NSIS_MODIFY_PATH@" "ON" 0 doNotAddToPath + StrCmp $DO_NOT_ADD_TO_PATH "1" doNotAddToPath 0 + Call AddToPath + doNotAddToPath: +SectionEnd + +;-------------------------------- +; Create custom pages +Function InstallOptionsPage + !insertmacro MUI_HEADER_TEXT "Install Options" "Choose options for installing @CPACK_NSIS_PACKAGE_NAME@" + !insertmacro MUI_INSTALLOPTIONS_DISPLAY "NSIS.InstallOptions.ini" + +FunctionEnd + +;-------------------------------- +; determine admin versus local install +Function un.onInit + + ClearErrors + UserInfo::GetName + IfErrors noLM + Pop $0 + UserInfo::GetAccountType + Pop $1 + StrCmp $1 "Admin" 0 +3 + SetShellVarContext all + ;MessageBox MB_OK 'User "$0" is in the Admin group' + Goto done + StrCmp $1 "Power" 0 +3 + SetShellVarContext all + ;MessageBox MB_OK 'User "$0" is in the Power Users group' + Goto done + + noLM: + ;Get installation folder from registry if available + + done: + +FunctionEnd + +;--- Add/Remove callback functions: --- +!macro SectionList MacroName + ;This macro used to perform operation on multiple sections. + ;List all of your components in following manner here. +@CPACK_NSIS_COMPONENT_SECTION_LIST@ +!macroend + +Section -FinishComponents + ;Removes unselected components and writes component status to registry + !insertmacro SectionList "FinishSection" + +!ifdef CPACK_NSIS_ADD_REMOVE + ; Get the name of the installer executable + System::Call 'kernel32::GetModuleFileNameA(i 0, t .R0, i 1024) i r1' + StrCpy $R3 $R0 + + ; Strip off the last 13 characters, to see if we have AddRemove.exe + StrLen $R1 $R0 + IntOp $R1 $R0 - 13 + StrCpy $R2 $R0 13 $R1 + StrCmp $R2 "AddRemove.exe" addremove_installed + + ; We're not running AddRemove.exe, so install it + CopyFiles $R3 $INSTDIR\AddRemove.exe + + addremove_installed: +!endif +SectionEnd +;--- End of Add/Remove callback functions --- + +;-------------------------------- +; Component dependencies +Function .onSelChange + !insertmacro SectionList MaybeSelectionChanged +FunctionEnd + +;-------------------------------- +;Uninstaller Section + +Section "Uninstall" + ReadRegStr $START_MENU SHCTX \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "StartMenu" + ;MessageBox MB_OK "Start menu is in: $START_MENU" + ReadRegStr $DO_NOT_ADD_TO_PATH SHCTX \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "DoNotAddToPath" + ReadRegStr $ADD_TO_PATH_ALL_USERS SHCTX \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "AddToPathAllUsers" + ReadRegStr $ADD_TO_PATH_CURRENT_USER SHCTX \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "AddToPathCurrentUser" + ;MessageBox MB_OK "Add to path: $DO_NOT_ADD_TO_PATH all users: $ADD_TO_PATH_ALL_USERS" + ReadRegStr $INSTALL_DESKTOP SHCTX \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "InstallToDesktop" + ;MessageBox MB_OK "Install to desktop: $INSTALL_DESKTOP " + +@CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS@ + + ;Remove files we installed. + ;Keep the list of directories here in sync with the File commands above. +@CPACK_NSIS_DELETE_FILES@ +@CPACK_NSIS_DELETE_DIRECTORIES@ + +!ifdef CPACK_NSIS_ADD_REMOVE + ;Remove the add/remove program + Delete "$INSTDIR\AddRemove.exe" +!endif + + ;Remove the uninstaller itself. + Delete "$INSTDIR\Uninstall.exe" + DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" + + ;Remove the installation directory if it is empty. + RMDir "$INSTDIR" + + ; Remove the registry entries. + DeleteRegKey SHCTX "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" + + ; Removes all optional components + !insertmacro SectionList "RemoveSection_CPack" + + !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP + + Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk" +@CPACK_NSIS_DELETE_ICONS@ +@CPACK_NSIS_DELETE_ICONS_EXTRA@ + + ;Delete empty start menu parent directories + StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" + + startMenuDeleteLoop: + ClearErrors + RMDir $MUI_TEMP + GetFullPathName $MUI_TEMP "$MUI_TEMP\.." + + IfErrors startMenuDeleteLoopDone + + StrCmp "$MUI_TEMP" "$SMPROGRAMS" startMenuDeleteLoopDone startMenuDeleteLoop + startMenuDeleteLoopDone: + + ; If the user changed the shortcut, then untinstall may not work. This should + ; try to fix it. + StrCpy $MUI_TEMP "$START_MENU" + Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk" +@CPACK_NSIS_DELETE_ICONS_EXTRA@ + + ;Delete empty start menu parent directories + StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" + + secondStartMenuDeleteLoop: + ClearErrors + RMDir $MUI_TEMP + GetFullPathName $MUI_TEMP "$MUI_TEMP\.." + + IfErrors secondStartMenuDeleteLoopDone + + StrCmp "$MUI_TEMP" "$SMPROGRAMS" secondStartMenuDeleteLoopDone secondStartMenuDeleteLoop + secondStartMenuDeleteLoopDone: + + DeleteRegKey /ifempty SHCTX "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" + + Push $INSTDIR\bin + StrCmp $DO_NOT_ADD_TO_PATH_ "1" doNotRemoveFromPath 0 + Call un.RemoveFromPath + doNotRemoveFromPath: +SectionEnd + +;-------------------------------- +; determine admin versus local install +; Is install for "AllUsers" or "JustMe"? +; Default to "JustMe" - set to "AllUsers" if admin or on Win9x +; This function is used for the very first "custom page" of the installer. +; This custom page does not show up visibly, but it executes prior to the +; first visible page and sets up $INSTDIR properly... +; Choose different default installation folder based on SV_ALLUSERS... +; "Program Files" for AllUsers, "My Documents" for JustMe... + +Function .onInit + StrCmp "@CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL@" "ON" 0 inst + + ReadRegStr $0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "UninstallString" + StrCmp $0 "" inst + + MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION \ + "@CPACK_NSIS_PACKAGE_NAME@ is already installed. $\n$\nDo you want to uninstall the old version before installing the new one?" \ + /SD IDYES IDYES uninst IDNO inst + Abort + +;Run the uninstaller +uninst: + ClearErrors + StrLen $2 "\Uninstall.exe" + StrCpy $3 $0 -$2 # remove "\Uninstall.exe" from UninstallString to get path + ExecWait '"$0" /S _?=$3' ;Do not copy the uninstaller to a temp file + + IfErrors uninst_failed inst +uninst_failed: + MessageBox MB_OK|MB_ICONSTOP "Uninstall failed." + Abort + + +inst: + ; Reads components status for registry + !insertmacro SectionList "InitSection" + + ; check to see if /D has been used to change + ; the install directory by comparing it to the + ; install directory that is expected to be the + ; default + StrCpy $IS_DEFAULT_INSTALLDIR 0 + StrCmp "$INSTDIR" "@CPACK_NSIS_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_DIRECTORY@" 0 +2 + StrCpy $IS_DEFAULT_INSTALLDIR 1 + + StrCpy $SV_ALLUSERS "JustMe" + ; if default install dir then change the default + ; if it is installed for JustMe + StrCmp "$IS_DEFAULT_INSTALLDIR" "1" 0 +2 + StrCpy $INSTDIR "$DOCUMENTS\@CPACK_PACKAGE_INSTALL_DIRECTORY@" + + ClearErrors + UserInfo::GetName + IfErrors noLM + Pop $0 + UserInfo::GetAccountType + Pop $1 + StrCmp $1 "Admin" 0 +4 + SetShellVarContext all + ;MessageBox MB_OK 'User "$0" is in the Admin group' + StrCpy $SV_ALLUSERS "AllUsers" + Goto done + StrCmp $1 "Power" 0 +4 + SetShellVarContext all + ;MessageBox MB_OK 'User "$0" is in the Power Users group' + StrCpy $SV_ALLUSERS "AllUsers" + Goto done + + noLM: + StrCpy $SV_ALLUSERS "AllUsers" + ;Get installation folder from registry if available + + done: + StrCmp $SV_ALLUSERS "AllUsers" 0 +3 + StrCmp "$IS_DEFAULT_INSTALLDIR" "1" 0 +2 + StrCpy $INSTDIR "@CPACK_NSIS_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_DIRECTORY@" + + StrCmp "@CPACK_NSIS_MODIFY_PATH@" "ON" 0 noOptionsPage + !insertmacro MUI_INSTALLOPTIONS_EXTRACT "NSIS.InstallOptions.ini" + + noOptionsPage: +FunctionEnd diff --git a/cmake/cmake-boilerplate.cmake b/cmake/cmake-boilerplate.cmake new file mode 100644 index 0000000..80d9edf --- /dev/null +++ b/cmake/cmake-boilerplate.cmake @@ -0,0 +1,139 @@ +# This macro is meant to be called once for all modules, since it's a set of spells +# that all modules will have to cast to be installed properly. +# It requires arguments in this order: +# - _name : The name of the generated library +# +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +include(GNUInstallDirs) + +macro(init_module) + set(_namespace "superflow") + set(_module "${PROJECT_NAME}") + set(target_name "${_namespace}-${_module}") + + message(STATUS "* Init module '${_module}'") +endmacro() + +macro(add_library_boilerplate) + set(gcc_compiler_flags + -Wall + -Wcast-align + -Wcast-qual + -Werror + -Wextra + -Wfloat-conversion + -Winit-self + -Winit-self + -Wlogical-op + -Wmissing-declarations + -Wnon-virtual-dtor + -Wold-style-cast + -Woverloaded-virtual + -Wpedantic + -Wpointer-arith + -Wshadow + -Wsuggest-override + -Wuninitialized + -Wunknown-pragmas + -Wunreachable-code + -Wunused-local-typedefs + ) + + set(msvc_compiler_flags + /W4 + /WX + ) + + list(APPEND built_components ${target_name}) + set(built_components ${built_components} PARENT_SCOPE) + + message(STATUS "* Add cmake boilerplate for target '${target_name}'") + + file(GLOB_RECURSE HEADER_FILES include/*.h) + file(GLOB_RECURSE SRC_FILES src/*.cpp) + + add_library(${target_name} + ${HEADER_FILES} + ${SRC_FILES} + ) + + add_library(${_namespace}::${_module} ALIAS ${target_name}) + + set(msvc_cxx "$") + set(gcc_like_cxx "$") + set(config_coverage "$") + set(add_coverage "$") + + target_compile_options(${target_name} PRIVATE + "$<${gcc_like_cxx}:$>" + "$<${msvc_cxx}:$>" + ) + + target_compile_options(${target_name} PUBLIC "$<${add_coverage}:--coverage;-g;-O0>") + target_link_options( ${target_name} PUBLIC "$<${add_coverage}:--coverage>") + + target_include_directories(${target_name} + PRIVATE $ + ) + + target_include_directories(${target_name} + SYSTEM INTERFACE + $ + $ + ) + + set_target_properties(${target_name} PROPERTIES + LINKER_LANGUAGE CXX + CXX_STANDARD_REQUIRED ON + CXX_STANDARD 17 + POSITION_INDEPENDENT_CODE ON + BUILD_RPATH $ORIGIN + INSTALL_RPATH $ORIGIN + WINDOWS_EXPORT_ALL_SYMBOLS ON + ) + + # Prepend the top-level project name to all installed library files + string(TOLOWER "${target_name}" output_name) + set_target_properties(${target_name} PROPERTIES + EXPORT_NAME ${_module} + OUTPUT_NAME ${output_name} + ) + + common_boilerplate() + +endmacro() + +macro(common_boilerplate) + set(project_config_in "${CMAKE_CURRENT_LIST_DIR}/${_module}-config.cmake.in") + set(project_config_out "${CMAKE_BINARY_DIR}/${_namespace}-${_module}-config.cmake") + + configure_package_config_file( + ${project_config_in} + ${project_config_out} + INSTALL_DESTINATION ${config_install_dir} + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO + ) + + install(FILES + ${project_config_out} + DESTINATION ${config_install_dir} + COMPONENT ${_module} + ) + + install(TARGETS ${target_name} + EXPORT ${targets_export_name} + COMPONENT ${_module} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + ) + + install( + DIRECTORY "include/" + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + COMPONENT ${_module} + FILES_MATCHING PATTERN "*.h" + ) +endmacro() diff --git a/cmake/config.cmake.in b/cmake/config.cmake.in new file mode 100644 index 0000000..da616a1 --- /dev/null +++ b/cmake/config.cmake.in @@ -0,0 +1,53 @@ +# This file is autogenerated at the call of `make` or `make install` of superflow. +# Use it when you want to `find_package(superflow CONFIG)`. +# +# This file will set the following variables for you to use: +# superflow_FOUND +# superflow_VERSION +# superflow_LIBS +# superflow_LIBRARIES +# +# For each module that was enabled at the call of `make install`, a variable +# superflow__FOUND will be set to TRUE, so that you may use +# find_package(superflow COMPONENTS ). +# +# If COMPONENTS is specified, +# superflow_LIBS will be a list of the targets corresponding to the requested components. +# Else, +# superflow_LIBS will be a list of all compiled superflow targets. +# In any case, +# superflow::core will be included in superflow_LIBS. +# +# superflow_LIBRARIES is exactly the same as superflow_LIBS. +# +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +@PACKAGE_INIT@ +include("${CMAKE_CURRENT_LIST_DIR}/superflow-targets.cmake") + +message(STATUS "* Loading @PROJECT_NAME@ v${@PROJECT_NAME@_VERSION}: " "${CMAKE_CURRENT_LIST_FILE}") + +list(APPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_LIST_DIR}) +include(CMakeFindDependencyMacro) + +if(superflow_FIND_COMPONENTS) + set(requested_components core;${superflow_FIND_COMPONENTS}) + #string(REGEX REPLACE "([a-z]+)(;|$)" "superflow-\\1\\2" requested_components "${requested_components}") +else() + set(requested_components @built_components@) + string(REGEX REPLACE "superflow-([a-z]+)(;|$)" "\\1\\2" requested_components "${requested_components}") +endif() +list(REMOVE_DUPLICATES requested_components) + +string(REPLACE ";" " " spaced_components "${requested_components}") +message(STATUS "* Requested superflow components: ${spaced_components}") + +foreach(comp IN LISTS requested_components) + find_dependency(superflow-${comp}) + list(APPEND superflow_LIBS "superflow::${comp}") +endforeach() + +set(superflow_LIBRARIES ${superflow_LIBS}) + +check_required_components(superflow) +message(STATUS "* Found @PROJECT_NAME@ v${@PROJECT_NAME@_VERSION} ${spaced_components}") diff --git a/cmake/enable-tests.cmake b/cmake/enable-tests.cmake new file mode 100644 index 0000000..6b528a3 --- /dev/null +++ b/cmake/enable-tests.cmake @@ -0,0 +1,17 @@ +set(gtest_tag "v1.14.0") +message(STATUS "* Fetch content GTest ${gtest_tag}") +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG ${gtest_tag} +) + +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) +set(BUILD_GTEST ON CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(googletest) + +enable_testing() +message(STATUS "* GTest fetched") diff --git a/cmake/nsis-banner.bmp b/cmake/nsis-banner.bmp new file mode 100644 index 0000000000000000000000000000000000000000..759afe343417a88836a7a4a4439856f8036dcf7d GIT binary patch literal 25818 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 literal 0 HcmV?d00001 diff --git a/cmake/nsis-welcome.bmp b/cmake/nsis-welcome.bmp new file mode 100644 index 0000000000000000000000000000000000000000..1d420dfde87aa38979c34c22f9534443ccc6d95c GIT binary patch literal 154542 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 literal 0 HcmV?d00001 diff --git a/cmake/packaging.cmake b/cmake/packaging.cmake new file mode 100644 index 0000000..d006f5b --- /dev/null +++ b/cmake/packaging.cmake @@ -0,0 +1,78 @@ +macro(set_backslash _dst_var _string) + string(REPLACE "/" "\\" ${_dst_var} ${_string}) +endmacro() + +macro(unset_provides_conflicts_replaces _component) + string(TOUPPER ${_component} _COMPONENT) + set(CPACK_DEBIAN_${_COMPONENT}_PACKAGE_PROVIDES "") + set(CPACK_DEBIAN_${_COMPONENT}_PACKAGE_CONFLICTS "") + set(CPACK_DEBIAN_${_COMPONENT}_PACKAGE_REPLACES "") +endmacro() + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) +list(REMOVE_DUPLICATES CMAKE_MODULE_PATH) + +set(CPACK_PACKAGE_CONTACT "Ragnar Smestad ") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "An efficient processing framework for modern C++") +set(CPACK_PACKAGE_DESCRIPTION +"The Superflow is the informational space between universes. + It also serves as the place where dreams and ideas come from, and from where telepathy operates. (...). + The laws of physics do not apply in the Superflow.") +set(CPACK_PACKAGE_INSTALL_DIRECTORY "superflow") +set(CPACK_PACKAGE_VENDOR "FFI (Forsvarets forskningsinstitutt)") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE") +set(CPACK_VERBATIM_VARIABLES TRUE) + +set(CPACK_DEBIAN_PACKAGE_PROVIDES "superflow-core,superflow-curses,superflow-loader,superflow-yaml") +set(CPACK_DEBIAN_PACKAGE_CONFLICTS "superflow-core,superflow-curses,superflow-loader,superflow-yaml") +set(CPACK_DEBIAN_PACKAGE_REPLACES "superflow-core,superflow-curses,superflow-loader,superflow-yaml") +unset_provides_conflicts_replaces("CORE") +unset_provides_conflicts_replaces("CURSES") +unset_provides_conflicts_replaces("LOADER") +unset_provides_conflicts_replaces("YAML") + +set(CPACK_DEBIAN_CORE_PACKAGE_DEPENDS "build-essential") +set(CPACK_DEBIAN_CURSES_PACKAGE_DEPENDS "libncurses-dev") +set(CPACK_DEBIAN_LOADER_PACKAGE_DEPENDS "libboost-filesystem-dev") +set(CPACK_DEBIAN_YAML_PACKAGE_DEPENDS "libyaml-cpp-dev") + +set(CPACK_DEB_COMPONENT_INSTALL ON) +set(CPACK_DEBIAN_ENABLE_COMPONENT_DEPENDS ON) + +set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) +set(CPACK_DEBIAN_PACKAGE_DEPENDS "build-essential,libboost-filesystem-dev,libncurses-dev,libyaml-cpp-dev") +set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/ffi-no/superflow") +set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") +set(CPACK_DEBIAN_PACKAGE_VERSION "${${PROJECT_NAME}_VERSION}") +set(CPACK_DEBIAN_PACKAGE_RELEASE "0") +set(CPACK_DEBIAN_PACKAGE_SECTION "libs") +set(CPACK_DEBIAN_PACKAGE_DESCRIPTION +"${CPACK_PACKAGE_DESCRIPTION_SUMMARY}. + ${CPACK_PACKAGE_DESCRIPTION}" +) + +set(CPACK_NSIS_CONTACT "Ragnar Smestad ") +set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) +set(CPACK_NSIS_INSTALL_ROOT "C:\\local") +set(CPACK_NSIS_PACKAGE_NAME "superflow") +set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/cmake/nsis-icon.ico") +set(CPACK_NSIS_MUI_UNIICON "${CMAKE_SOURCE_DIR}/cmake/nsis-icon.ico") +set_backslash(CPACK_NSIS_MUI_HEADERIMAGE_BITMAP "${CMAKE_SOURCE_DIR}/cmake/nsis-banner.bmp") +set_backslash(CPACK_NSIS_MUI_WELCOMEFINISHPAGE_BITMAP "${CMAKE_SOURCE_DIR}/cmake/nsis-welcome.bmp") + +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(CPACK_GENERATOR "NSIS64") +else() + set(CPACK_GENERATOR "DEB") +endif() + +#set(CPACK_COMPONENTS_GROUPING "ONE_PER_GROUP") +#set(CPACK_COMPONENTS_GROUPING "ALL_COMPONENTS_IN_ONE") +#set(CPACK_COMPONENTS_GROUPING "IGNORE") + +include(CPack) + +cpack_add_component(core DISPLAY_NAME core DESCRIPTION "The flow::core library" GROUP dev) +cpack_add_component(curses DISPLAY_NAME curses DESCRIPTION "The flow::curses library" GROUP dev DEPENDS core) +cpack_add_component(loader DISPLAY_NAME loader DESCRIPTION "The flow::loader library" GROUP dev DEPENDS core) +cpack_add_component(yaml DISPLAY_NAME yaml DESCRIPTION "The flow::yaml library" GROUP dev DEPENDS core) diff --git a/cmake/uninstall.cmake b/cmake/uninstall.cmake new file mode 100644 index 0000000..694a1ed --- /dev/null +++ b/cmake/uninstall.cmake @@ -0,0 +1,26 @@ +# https://gist.github.com/royvandam/3033428 + +set(MANIFEST "${CMAKE_CURRENT_BINARY_DIR}/install_manifest.txt") + +if(NOT EXISTS ${MANIFEST}) + message(FATAL_ERROR "Cannot find install manifest: '${MANIFEST}'") +endif() + +file(STRINGS ${MANIFEST} files) +foreach(file ${files}) + if(EXISTS ${file}) + message(STATUS "Removing file: '${file}'") + + exec_program( + ${CMAKE_COMMAND} ARGS "-E remove ${file}" + OUTPUT_VARIABLE stdout + RETURN_VALUE result + ) + + if(NOT "${result}" STREQUAL 0) + message(FATAL_ERROR "Failed to remove file: '${file}'.") + endif() + else() + MESSAGE(STATUS "File '${file}' does not exist.") + endif() +endforeach(file) diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..6fc7df5 --- /dev/null +++ b/conanfile.py @@ -0,0 +1,152 @@ +from conan import ConanFile +from conan.errors import ConanInvalidConfiguration +from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps +from conan.tools.files import load +from conan.tools.system.package_manager import Apt + +required_conan_version = ">=1.60.0 <2.0 || >=2.0.5" + +class superflowRecipe(ConanFile): + name = "superflow" + package_type = "library" + + license = "MIT" + author = "FFI" + homepage = "https://github.com/ffi-no/superflow" + url = homepage + description = "An efficient processing framework for modern C++" + topics = ("c++") + + # Binary configuration + settings = "os", "compiler", "build_type", "arch" + options = { + "all": [False, True], + "curses": [False, True], + "loader": [False, True], + "yaml": [False, True], + "shared": [False, True], + "tests": [False, True], + } + default_options = { + "all": False, + "shared": False, + "tests": False, + } + exports_sources = ( + "CMakeLists.txt", + "LICENSE", + "cmake/*", + "core/*", + "curses/*", + "loader/*", + "yaml/*", + ) + + def set_version(self): + import re + import os + self.version = re.search(r"project\(\S+ VERSION (\d+(\.\d+){1,3})", + load(self, os.path.join(self.recipe_folder, "CMakeLists.txt"))).group(1).strip() + + def validate(self): + if self.settings.os == "Windows" and self.options.curses: + raise ConanInvalidConfiguration("Windows not supported for module 'curses'") + + def config_options(self): + pass + + def configure(self): + if self.options.all: + self.options.curses = True + self.options.loader = True + self.options.yaml = True + + if self.options.loader: + self.options.yaml = True + + for lib_name in ['curses', 'loader', 'yaml']: + if not getattr(self.options, lib_name): + setattr(self.options, lib_name, False) + + opts = sorted(self.options._package_options._data) + width = len(max(opts, key=len)) + 1 + self.output.info("superflow options configured:\n" + "\n".join([f" {o: <{width}}: {str(getattr(self.options, o))}" for o in opts])) + + def generate(self): + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.variables["BUILD_SHARED_LIBS"] = self.options.shared + tc.variables["BUILD_TESTS"] = self.options.tests + tc.variables["BUILD_all"] = self.options.all + tc.variables["BUILD_curses"] = self.options.curses + tc.variables["BUILD_loader"] = self.options.loader + tc.variables["BUILD_yaml"] = self.options.yaml + tc.generate() + + def requirements(self): + if self.options.curses: + self.requires("ncursescpp/1.0.0") + + if self.options.loader: + self.requires("boost/1.76.0", transitive_headers=True) + + if self.options.yaml: + self.requires("yaml-cpp/0.8.0", transitive_headers=True) + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + if self.options.tests: + self.run(f"ctest --output-on-failure --output-junit report.xml") + + def build_requirements(self): + self.tool_requires("cmake/3.27.4") + + def package(self): + cmake = CMake(self) + cmake.install() + + def layout(self): + cmake_layout(self) + + def package_info(self): + for lib_name in ['core', 'curses', 'loader', 'yaml']: + if lib_name == 'core' or getattr(self.options, lib_name): + self.output.info("adding component '{}'".format(lib_name)) + self.cpp_info.components[lib_name].set_property("cmake_target_name", f"{self.name}::{lib_name}") + self.cpp_info.components[lib_name].set_property("cmake_file_name", f"{self.name}-{lib_name}") + self.cpp_info.components[lib_name].libs = ['superflow-' + lib_name] + if lib_name != 'core': + self.cpp_info.components[lib_name].requires.append('core') + + if self.settings.os == "Linux": + self.cpp_info.components['core'].system_libs = ["pthread"] + + if self.options.curses: + self.cpp_info.components['curses'].requires.append('ncursescpp::ncursescpp') + + if self.options.loader: + self.cpp_info.components['loader'].requires.append('boost::boost') + if self.options.yaml: + self.cpp_info.components['loader'].requires.append('yaml') + if self.settings.os == "Linux": + self.cpp_info.components['loader'].system_libs.append('dl') + + if self.options.yaml: + self.cpp_info.components['yaml'].requires.append('yaml-cpp::yaml-cpp') + self.cpp_info.components['yaml'].defines = [ + 'LOADER_ADAPTER_HEADER=\"superflow/yaml/yaml_property_list.h\"', + 'LOADER_ADAPTER_NAME=YAML', + 'LOADER_ADAPTER_TYPE=flow::yaml::YAMLPropertyList', + ] + + if self.settings.build_type == "Debug": + for component in self.cpp_info.components.values(): + for i, lib in enumerate(component.libs): + component.libs[i] = lib + 'd' + + for k, v in self.cpp_info.components.items(): + self.output.info("{:<11} -> {}".format("{} libs".format(k), v.libs)) + self.output.info("{:<11} -> {}".format("{} requires".format(k), v.requires)) diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt new file mode 100644 index 0000000..b6a8e89 --- /dev/null +++ b/core/CMakeLists.txt @@ -0,0 +1,15 @@ +project(core) +init_module() + +find_package(Threads REQUIRED) + +add_library_boilerplate() + +target_link_libraries(${target_name} + PUBLIC + Threads::Threads +) + +if (BUILD_TESTS) + add_subdirectory(test) +endif () diff --git a/core/core-config.cmake.in b/core/core-config.cmake.in new file mode 100644 index 0000000..4ae11aa --- /dev/null +++ b/core/core-config.cmake.in @@ -0,0 +1,9 @@ +@PACKAGE_INIT@ +message(STATUS "* Found @CMAKE_PROJECT_NAME@::@PROJECT_NAME@: " "${CMAKE_CURRENT_LIST_FILE}") +include(CMakeFindDependencyMacro) +find_dependency(Threads) + +set(@CMAKE_PROJECT_NAME@_@PROJECT_NAME@_FOUND TRUE) + +check_required_components(@PROJECT_NAME@) +message(STATUS "* Loading @CMAKE_PROJECT_NAME@::@PROJECT_NAME@ complete") diff --git a/core/include/superflow/buffered_consumer_port.h b/core/include/superflow/buffered_consumer_port.h new file mode 100644 index 0000000..d8ac2af --- /dev/null +++ b/core/include/superflow/buffered_consumer_port.h @@ -0,0 +1,241 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/consumer_port.h" +#include "superflow/connection_manager.h" +#include "superflow/policy.h" +#include "superflow/port.h" +#include "superflow/queue_getter.h" +#include "superflow/utils/data_stream.h" +#include "superflow/utils/lock_queue.h" + +namespace flow +{ +/// \brief +/// +/// The port has a buffer with configurable size containing data received from the producer. +/// \tparam T The type of data to be exchanged between ports. +/// \tparam P ConnectPolicy, default is Single +/// \tparam M GetMode, default is Blocking +/// \tparam L LeakPolicy, default is Leaky +/// \tparam Variants... Optionally supported input variant types. \see ConsumerPort +template< + typename T, + ConnectPolicy P = ConnectPolicy::Single, + GetMode M = GetMode::Blocking, + LeakPolicy L = LeakPolicy::Leaky, + typename... Variants +> +class BufferedConsumerPort final : + public ConsumerPort, + public DataStream, + public std::enable_shared_from_this +{ +public: + using Ptr = std::shared_ptr; + explicit BufferedConsumerPort(unsigned int buffer_size = 1); + + void receive(const T&, const Port::Ptr&) override; + + void connect(const Port::Ptr& ptr) override; + + void disconnect() noexcept override; + + void disconnect(const Port::Ptr& ptr) noexcept override; + + bool isConnected() const override; + + std::optional getNext() override; + + /// \brief Returns true if the buffer is not empty + bool hasNext() const; + + /// \brief Check if the buffer is terminated or not + /// \return False if the buffer is terminated. + operator bool() const override; + + /// \brief Empties the internal buffer, any unread data will be discarded. + void clear(); + + void deactivate(); + + PortStatus getStatus() const; + + size_t getQueueSize() const; + +private: + size_t num_transactions_ = 0; + LockQueue buffer_; + ConnectionManager

connection_manager_; + QueueGetter queue_getter_; +}; + +// ----- Implementations ----- +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +BufferedConsumerPort::BufferedConsumerPort(const unsigned int buffer_size) + : buffer_(buffer_size) +{} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +void BufferedConsumerPort::receive(const T& item, const Port::Ptr&) +{ + if (!buffer_.isTerminated()) + { + try { buffer_.push(item); } + catch(const flow::TerminatedException&) {} + } +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +void BufferedConsumerPort::connect(const Port::Ptr& ptr) +{ + connection_manager_.connect(shared_from_this(), ptr); +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +void BufferedConsumerPort::disconnect() noexcept +{ + connection_manager_.disconnect(shared_from_this()); +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +void BufferedConsumerPort::disconnect(const Port::Ptr& ptr) noexcept +{ + connection_manager_.disconnect(shared_from_this(), ptr); +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +bool BufferedConsumerPort::isConnected() const +{ + return connection_manager_.isConnected(); +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +std::optional BufferedConsumerPort::getNext() +{ + const auto item = queue_getter_.get(buffer_); + + if (item) + { ++num_transactions_; } + + return item; +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +bool BufferedConsumerPort::hasNext() const +{ + return queue_getter_.hasNext(buffer_); +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +BufferedConsumerPort::operator bool() const +{ + return !buffer_.isTerminated(); +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +PortStatus BufferedConsumerPort::getStatus() const +{ + return { + connection_manager_.getNumConnections(), + num_transactions_ + }; +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +size_t BufferedConsumerPort::getQueueSize() const +{ + return buffer_.getQueueSize(); +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +void BufferedConsumerPort::clear() +{ + buffer_.clearQueue(); + queue_getter_.clear(); +} + +template< + typename T, + ConnectPolicy P, + GetMode M, + LeakPolicy L, + typename... Variants +> +void BufferedConsumerPort::deactivate() +{ + buffer_.terminate(); +} +} diff --git a/core/include/superflow/callback_consumer_port.h b/core/include/superflow/callback_consumer_port.h new file mode 100644 index 0000000..49cb9f6 --- /dev/null +++ b/core/include/superflow/callback_consumer_port.h @@ -0,0 +1,123 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/connection_manager.h" +#include "superflow/consumer_port.h" +#include "superflow/policy.h" + +#include + +namespace flow +{ +/// \brief This port calls a function every time data is received. +/// \tparam T The type of data to be exchanged between ports. +/// \tparam P ConnectPolicy +/// \tparam Variants... Optionally supported input variant types. \see ConsumerPort +template< + typename T, + ConnectPolicy P = ConnectPolicy::Single, + typename... Variants +> +class CallbackConsumerPort : + public ConsumerPort, + public std::enable_shared_from_this +{ +public: + using Ptr = std::shared_ptr; + using Callback = std::function; + + explicit CallbackConsumerPort(const Callback&); + + void receive(const T&, const Port::Ptr&) override; + + void connect(const Port::Ptr& ptr) override; + + void disconnect() noexcept override; + + void disconnect(const Port::Ptr& ptr) noexcept override; + + bool isConnected() const override; + + PortStatus getStatus() const override; + +private: + size_t num_transactions_ = 0; + + Callback callback_; + ConnectionManager

connection_manager_; +}; + +// --- Implementations --- // +template< + typename T, + ConnectPolicy P, + typename... Variants +> +CallbackConsumerPort::CallbackConsumerPort(const Callback& callback) + : callback_{callback} +{} + +template< + typename T, + ConnectPolicy P, + typename... Variants +> +inline void CallbackConsumerPort::receive(const T& t, const Port::Ptr&) +{ + callback_(t); + ++num_transactions_; +} + +template< + typename T, + ConnectPolicy P, + typename... Variants +> +void CallbackConsumerPort::connect(const Port::Ptr& ptr) +{ + connection_manager_.connect(shared_from_this(), ptr); +} + +template< + typename T, + ConnectPolicy P, + typename... Variants +> +void CallbackConsumerPort::disconnect() noexcept +{ + connection_manager_.disconnect(shared_from_this()); +} + +template< + typename T, + ConnectPolicy P, + typename... Variants +> +void CallbackConsumerPort::disconnect(const Port::Ptr& ptr) noexcept +{ + connection_manager_.disconnect(shared_from_this(), ptr); +} + +template< + typename T, + ConnectPolicy P, + typename... Variants +> +bool CallbackConsumerPort::isConnected() const +{ + return connection_manager_.isConnected(); +} + +template< + typename T, + ConnectPolicy P, + typename... Variants +> +PortStatus CallbackConsumerPort::getStatus() const +{ + return { + connection_manager_.getNumConnections(), + num_transactions_ + }; +} +} diff --git a/core/include/superflow/connection_manager.h b/core/include/superflow/connection_manager.h new file mode 100644 index 0000000..29e3304 --- /dev/null +++ b/core/include/superflow/connection_manager.h @@ -0,0 +1,181 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/policy.h" +#include "superflow/port.h" + +#include +#include + +namespace flow +{ +/// \brief A utility class for handling connections between +/// Ports, while also encforcing ConnectPolicy. +/// +/// A ConnectionManager is typically owned by a Port to keep +/// track of its connections. +/// The class focuses on connections only, and is agnostic to communication. +/// \tparam P Supported ConnectPolicys are Single and Multi. +template +class ConnectionManager +{ +public: + /// \brief Register a new connection and call other->connect(owner). + /// \param owner Typically the owner of the ConnectionManager + /// \param other The external Port to connect to. + void connect( + const Port::Ptr& owner, + const Port::Ptr& other + ); + + /// \brief Disconnect from all registered connections. + /// \param owner Typically the owner of the ConnectionManager + void disconnect( + const Port::Ptr& owner + ) noexcept; + + /// \brief Disconnect a specific Port + /// \param owner Typically the owner of the ConnectionManager + /// \param other The external Port to disconnect from. + void disconnect( + const Port::Ptr& owner, + const Port::Ptr& other + ) noexcept; + + /// \brief Get the number of connection pairs managed by the ConnectionManager + size_t getNumConnections() const; + + /// \brief True if the ConnectionManager manages at least one connection. + bool isConnected() const; + +private: + std::unordered_set connections_; + + bool hasPort(const Port::Ptr& ptr) const; + + Port::Ptr front() const; +}; + +template<> +inline bool ConnectionManager::hasPort( + const Port::Ptr& ptr +) const +{ + return connections_.find(ptr) != connections_.end(); +} + +template<> +inline Port::Ptr ConnectionManager::front() const +{ + return connections_.empty() + ? nullptr + : *connections_.cbegin(); +} + +template<> +inline void ConnectionManager::connect( + const Port::Ptr& owner, + const Port::Ptr& other +) +{ + if (hasPort(other)) + { return; } + + connections_.insert(other); + + try + { + other->connect(owner); + } + catch (...) + { + connections_.erase(other); + std::rethrow_exception(std::current_exception()); + } +} + +template<> +inline void ConnectionManager::connect( + const Port::Ptr& owner, + const Port::Ptr& other +) +{ + if (other == front()) + { return; } + + if (!connections_.empty()) + { throw std::invalid_argument("Attempted connecting multiple ports to Single-port"); } + + connections_.insert(other); + + try + { + other->connect(owner); + } + catch (...) + { + connections_.clear(); + std::rethrow_exception(std::current_exception()); + } +} + +template +inline void ConnectionManager

::disconnect( + const Port::Ptr& owner +) noexcept +{ + std::unordered_set old_connections; + std::swap(connections_, old_connections); + + for (const auto& connection : old_connections) + { + connection->disconnect(owner); + } +} + +template<> +inline void ConnectionManager::disconnect( + const Port::Ptr& owner, + const Port::Ptr& other +) noexcept +{ + auto it = connections_.find(other); + + if (it == connections_.end()) + { + // already disconnected + return; + } + + connections_.erase(it); + other->disconnect(owner); +} + +template<> +inline void ConnectionManager::disconnect( + const Port::Ptr& owner, + const Port::Ptr& other +) noexcept +{ + if (other != front()) + { + // already disconnected + return; + } + + connections_.clear(); + other->disconnect(owner); +} + +template +inline size_t ConnectionManager

::getNumConnections() const +{ + return connections_.size(); +} + +template +inline bool ConnectionManager

::isConnected() const +{ + return getNumConnections() > 0; +} +} diff --git a/core/include/superflow/connection_spec.h b/core/include/superflow/connection_spec.h new file mode 100644 index 0000000..5269c54 --- /dev/null +++ b/core/include/superflow/connection_spec.h @@ -0,0 +1,15 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include + +namespace flow +{ +struct ConnectionSpec +{ + std::string lhs_name; + std::string lhs_port; + std::string rhs_name; + std::string rhs_port; +}; +} diff --git a/core/include/superflow/consumer_port.h b/core/include/superflow/consumer_port.h new file mode 100644 index 0000000..f3ae603 --- /dev/null +++ b/core/include/superflow/consumer_port.h @@ -0,0 +1,83 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/port.h" + +#include + +namespace flow +{ +namespace detail +{ +template +class Consumer +{ +public: + using Ptr = std::shared_ptr; + using Type = T; + + virtual ~Consumer() = default; + + /// \brief Function to be called by ProducerPort in order to send data to the ConsumerPort + /// \param data The data sent by ProducerPort + /// \param port Pointer to the ProducerPort sending data + virtual void receive(const T& data, const Port::Ptr& port) = 0; +}; + +template +class ConsumerVariant + : protected virtual Consumer + , public Consumer +{ +public: + static_assert( + std::is_convertible_v || std::is_constructible_v, + "Cannot create a ConsumerVariant with the Variant and Base types specified in your ConsumerPort because const Variant& is not convertible to const Base&." + ); + + void receive(const Variant& data, const Port::Ptr& port) final + { + using BaseConsumer = Consumer; + if constexpr (std::is_convertible_v) + { + static_cast(*this).receive(static_cast(data), port); + } + else + { + // Since const Variant& is not convertible to const Base&, Base must be + // constructible from const Variant&. This is less efficient than casting refs. + + static_cast(*this).receive(static_cast(data), port); + } + } +}; +} + +/// \brief Interface for input ports able to connect with ProducerPort. +/// \tparam T The base type (e.g. "output type") of data of the consumer. +/// \tparam Variants... Optional upstream variant types of T which are also to be accepted by +/// the consumer. Each such `Variants` must be const ref convertible to `T`, that is +/// `const Variant&` must [be convertible](https://en.cppreference.com/w/cpp/types/is_convertible) +/// to `const T&`. This is also useful for allowing upcasts from a derived class +/// to a parent baseclass. +/// \see ProducerPort +/// \see BufferedConsumerPort +/// \see CallbackConsumerPort +/// \see MultiConsumerPort +template +class ConsumerPort : + public Port, + public virtual detail::Consumer, + public detail::ConsumerVariant... +{ +public: + using Ptr = std::shared_ptr; +}; + +/// \brief Partial template specialization for the case when `T` is a [`std::variant`](https://en.cppreference.com/w/cpp/utility/variant). +/// For further details, \see ConsumerPort. +template +class ConsumerPort> : + public ConsumerPort, Variants...> +{}; +} diff --git a/core/include/superflow/factory.h b/core/include/superflow/factory.h new file mode 100644 index 0000000..abd819a --- /dev/null +++ b/core/include/superflow/factory.h @@ -0,0 +1,17 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/proxel.h" + +#include + +namespace flow +{ +/// \brief Function for creating a new Proxel +/// \param name[in] The name of the Proxel +/// \param properties[in] configuration properties for the Proxel +template +using Factory = std::function; +} diff --git a/core/include/superflow/factory_map.h b/core/include/superflow/factory_map.h new file mode 100644 index 0000000..4614348 --- /dev/null +++ b/core/include/superflow/factory_map.h @@ -0,0 +1,86 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/factory.h" + +#include +#include + +namespace flow +{ +/// \brief Container for mapping a Proxel type to its respective Factory +/// \tparam PropertyList +template +class FactoryMap +{ +public: + FactoryMap() = default; + + FactoryMap(FactoryMap&&) noexcept = default; + + FactoryMap(const FactoryMap&) = default; + + FactoryMap& operator=(FactoryMap&&) noexcept = default; + + FactoryMap& operator=(const FactoryMap&) = default; + + /// \brief Create a new FactoryMap + /// \param factories mapping between proxel type and factory. + explicit FactoryMap(const std::map>& factories); + + explicit FactoryMap(std::map>&& factories); + + /// \brief Get Factory for the given Proxel type. + /// \param type The name of the Proxel type + /// \return The Proxel type's Factory + const Factory& get(const std::string& type) const; + + /// \brief Concatenate two FactoryMaps + /// \param other the FactoryMap to combine with the current + /// \return A new, concatenated FactoryMap + FactoryMap operator+(const FactoryMap& other) const; + void operator+=(const FactoryMap& other); + + [[nodiscard]] bool empty() const { return factories_.empty(); } + +private: + std::map> factories_; +}; + +// ----- Implementation ----- + +template +FactoryMap::FactoryMap(const std::map>& factories) + : factories_{factories} +{} + +template +FactoryMap::FactoryMap(std::map>&& factories) + : factories_{std::move(factories)} +{} + +template +const Factory& FactoryMap::get(const std::string& type) const +{ + try + { return factories_.at(type); } + catch (std::exception&) + { throw std::invalid_argument({"FactoryMap failed to load factory '" + type + "'"}); } +} + +template +FactoryMap FactoryMap::operator+(const FactoryMap& other) const +{ + std::map> sum{factories_.begin(), factories_.end()}; + + sum.insert(other.factories_.begin(), other.factories_.end()); + + return FactoryMap{std::move(sum)}; +} + +template +void FactoryMap::operator+=(const FactoryMap& other) +{ + factories_.insert(other.factories_.begin(), other.factories_.end()); +} +} diff --git a/core/include/superflow/graph.h b/core/include/superflow/graph.h new file mode 100644 index 0000000..4fec1c8 --- /dev/null +++ b/core/include/superflow/graph.h @@ -0,0 +1,120 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/proxel.h" +#include "superflow/port.h" + +#include +#include +#include +#include + +namespace flow +{ + +/// \brief Processing graph responsible for starting, stopping and monitoring Proxels +/// +/// Graph is the manager of Proxels, mainly providing a data structure for them to exist +/// and in the current implementation providing them with worker threads. +/// Graph can also be queried for Proxel statuses to monitor workload and processing times. +/// \see Proxel +class Graph +{ +public: + /// A function which is called if a Proxel chrashes, and the graph handles exceptions. + /// \param proxel_name First argument, the name of the crashed proxel + /// \param what Second argument, the `what` of the caught exception. + using CrashLogger = std::function; + + Graph() = default; + + /// Constructor that creates graph with a predefined set of proxels. + /// Proxels will otherwise have to be added using the `add` method. + /// \param proxels map which maps unique names to Proxels. + explicit Graph(std::map proxels); + + Graph(Graph&&) = default; + + Graph(const Graph&) = delete; + + ~Graph(); + + /// Call the `start` method of every Proxel, using a new thread per element. + /// Threads are not detatched. + /// \param handle_exceptions true if Graph should catch exceptions from proxels + /// \param crash_logger function that logs error messages caught from proxels. Has effect only + /// if `handle_exceptions` is also `true`. + /// \see CrashLogger, defaultCrashLogger + void start( + bool handle_exceptions = true, + const CrashLogger& crash_logger = defaultCrashLogger + ); + + /// Call the `stop` method of every Proxel, expecting the proxel thread to terminate. + /// Threads are joined. + void stop(); + + /// \brief Add a new proxel to the Graph. + /// \param proxel_id Unique name for the Proxel + /// \param proxel pointer to the proxel + void add(const std::string& proxel_id, Proxel::Ptr&& proxel); + + /// \brief Request a pointer to a Proxel + /// \tparam ProxelType optional template argument in order to get a specific sub class of Proxel. + /// \param proxel_name Unique name of Proxel + /// \return A pointer to the requested Proxel + /// \throws std::invalid argument if Proxel does not exist or is not of requested type. + template + std::shared_ptr getProxel(const std::string& proxel_name) const; + + /// \brief Create a connection between ports in two Proxels. + /// \param proxel1 Unique name of the first Proxel + /// \param proxel1_port Unique name of the first Proxel's port + /// \param proxel2 Unique name of the second Proxel + /// \param proxel2_port Unique name of the second Proxel's port + void connect(const std::string& proxel1, const std::string& proxel1_port, + const std::string& proxel2, const std::string& proxel2_port) const; + + /// \brief Retreive the current status of all proxels. + /// \see ProxelStatusMap + /// \return + [[nodiscard]] ProxelStatusMap getProxelStatuses() const; + + /// A CrashLogger that prints the error message to std::cerr. + /// \see CrashLogger + static void defaultCrashLogger(const std::string& proxel_name, const std::string& what); + + /// A CrashLogger that does absolutely nothing + /// \see CrashLogger + static const CrashLogger quietCrashLogger; + +private: + std::map proxels_; + std::map crashes_; + + std::map proxel_threads_; + + [[nodiscard]] bool isRunning() const; +}; + +template +std::shared_ptr Graph::getProxel(const std::string& proxel_name) const +{ + const Proxel::Ptr raw_ptr = getProxel(proxel_name); // does not recurse infinitely + const auto ptr = std::dynamic_pointer_cast(raw_ptr); + + if (ptr == nullptr) + { throw std::invalid_argument({"Proxel '" + proxel_name + "' is not of requested type."}); } + + return ptr; +} + +template<> +inline Proxel::Ptr Graph::getProxel(const std::string& proxel_name) const +{ + try + { return proxels_.at(proxel_name); } + catch (...) + { throw std::invalid_argument(std::string("Proxel '" + proxel_name + "' does not exist")); } +} +} diff --git a/core/include/superflow/graph_factory.h b/core/include/superflow/graph_factory.h new file mode 100644 index 0000000..446963c --- /dev/null +++ b/core/include/superflow/graph_factory.h @@ -0,0 +1,53 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/connection_spec.h" +#include "superflow/factory_map.h" +#include "superflow/graph.h" +#include "superflow/proxel_config.h" + +namespace flow +{ +template +inline std::map createProxelsFromConfig( + const FactoryMap& factory_map, + const std::vector>& proxel_configurations) +{ + std::map proxels{}; + + for (const auto& config : proxel_configurations) + { + if (proxels.count(config.id) > 0) + { throw std::invalid_argument("Proxel with id '"+ config.id +"' is defined more than once."); } + + const auto& factory = factory_map.get(config.type); + try + { + proxels.emplace(config.id, factory(config.properties)); + } + catch(const std::exception& e) + { + throw std::runtime_error("Failed to create proxel '" + config.id + "': " + e.what()); + } + } + + return proxels; +} + +template +inline Graph createGraph( + const FactoryMap& factory_map, + const std::vector>& proxel_configurations, + const std::vector& connections +) +{ + Graph graph{ + createProxelsFromConfig(factory_map, proxel_configurations) + }; + + for (const auto& connection : connections) + { graph.connect(connection.lhs_name, connection.lhs_port, connection.rhs_name, connection.rhs_port); } + + return graph; +} +} diff --git a/core/include/superflow/interface_port.h b/core/include/superflow/interface_port.h new file mode 100644 index 0000000..f1ea6ea --- /dev/null +++ b/core/include/superflow/interface_port.h @@ -0,0 +1,207 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once +#include "superflow/connection_manager.h" +#include "superflow/port.h" + +#include +#include +#include + +namespace flow +{ +template +struct InterfacePort +{ + class Host : + public Port, + public std::enable_shared_from_this + { + public: + using Ptr = std::shared_ptr; + + explicit Host( + Interface& handle + ); + + ~Host() override = default; + + void connect(const Port::Ptr& ptr) override; + + void disconnect() noexcept override; + + void disconnect(const Port::Ptr& ptr) noexcept override; + + bool isConnected() const override; + + PortStatus getStatus() const override; + + const Interface& get() const; + + Interface& get(); + + private: + mutable std::atomic num_transactions_ = 0; + Interface& handle_; + ConnectionManager connection_manager_; + }; + + class Client : + public Port, + public std::enable_shared_from_this + { + public: + using Ptr = std::shared_ptr; + + ~Client() override = default; + + void connect(const Port::Ptr& ptr) override; + + void disconnect() noexcept override; + + void disconnect(const Port::Ptr& ptr) noexcept override; + + bool isConnected() const override; + + PortStatus getStatus() const override; + + const Interface& get() const; + + Interface& get(); + + private: + mutable std::atomic num_transactions_ = 0; + std::shared_ptr host_; + }; +}; + +// ----- Implementation ----- +template +InterfacePort::Host::Host(Interface& handle) + : handle_{handle} +{} + +template +void InterfacePort::Host::connect(const Port::Ptr& ptr) +{ + connection_manager_.connect(shared_from_this(), ptr); +} + +template +void InterfacePort::Host::disconnect() noexcept +{ + connection_manager_.disconnect(shared_from_this()); +} + +template +void InterfacePort::Host::disconnect(const Port::Ptr& ptr) noexcept +{ + connection_manager_.disconnect(shared_from_this(), ptr); +} + +template +bool InterfacePort::Host::isConnected() const +{ + return connection_manager_.isConnected(); +} + +template +PortStatus InterfacePort::Host::getStatus() const +{ + return { + connection_manager_.getNumConnections(), + num_transactions_ + }; +} + +template +const Interface& InterfacePort::Host::get() const +{ + ++num_transactions_; + if (not isConnected()) + { throw std::runtime_error("InterfacePort::Host has no connection."); } + + return handle_; +} + +template +Interface& InterfacePort::Host::get() +{ + ++num_transactions_; + if (not isConnected()) + { throw std::runtime_error("InterfacePort::Host has no connection."); } + return handle_; +} + +template +void InterfacePort::Client::connect(const Port::Ptr& ptr) +{ + if (host_ == ptr) + { return; } + + const auto host = std::dynamic_pointer_cast(ptr); + + if (host == nullptr) + { throw std::invalid_argument{std::string("Type mismatch when connecting ports")}; } + + if (isConnected()) + { disconnect(); } + + host_ = host; + ptr->connect(shared_from_this()); +} + +template +void InterfacePort::Client::disconnect() noexcept +{ + if (host_ == nullptr) + { return; } + + const auto host = std::move(host_); + host->disconnect(); +} + +template +void InterfacePort::Client::disconnect(const Port::Ptr& ptr) noexcept +{ + if (host_ != ptr) + { return; } + + disconnect(); +} + +template +bool InterfacePort::Client::isConnected() const +{ + return host_ != nullptr; +} + +template +PortStatus InterfacePort::Client::getStatus() const +{ + return { + static_cast(isConnected()), + num_transactions_ + }; +} + +template +const Interface& InterfacePort::Client::get() const +{ + ++num_transactions_; + + if (not isConnected()) + { throw std::runtime_error("InterfacePort::Client has no connection."); } + + return host_->get(); +} + +template +Interface& InterfacePort::Client::get() +{ + ++num_transactions_; + if (not isConnected()) + { throw std::runtime_error("InterfacePort::Client has no connection."); } + + return host_->get(); +} +} diff --git a/core/include/superflow/mapped_asset_manager.h b/core/include/superflow/mapped_asset_manager.h new file mode 100644 index 0000000..5f5ff80 --- /dev/null +++ b/core/include/superflow/mapped_asset_manager.h @@ -0,0 +1,112 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include +#include +#include +#include + +namespace flow +{ +template +class MappedAssetManager +{ +public: + void put(const K& key, const V& value); + + V get(const K& key) const; + + bool has(const K& key) const; + + void erase(const K& key); + + void clear(); + + std::vector getAll() const; + +private: + mutable std::mutex mutex_; + std::map values_; +}; + +// ----- Implementation ----- +template +void MappedAssetManager::put(const K& key, const V& value) +{ + std::lock_guard lock{mutex_}; + + if (values_.find(key) != values_.end()) + { + return; + } + + values_[key] = value; +} + +template +V MappedAssetManager::get(const K& key) const +{ + V v; + + { + std::lock_guard lock{mutex_}; + + auto it = values_.find(key); + + if (it == values_.end()) + { + throw std::invalid_argument("Attempted accessing non-existing element"); + } + + v = it->second; + } + + return v; +} + +template +bool MappedAssetManager::has(const K& key) const +{ + std::lock_guard lock{mutex_}; + + return values_.find(key) != values_.end(); +} + +template +void MappedAssetManager::erase(const K& key) +{ + std::lock_guard lock{mutex_}; + + auto it = values_.find(key); + + if (it == values_.end()) + { + return; + } + + values_.erase(it); +} + +template +void MappedAssetManager::clear() +{ + std::lock_guard lock{mutex_}; + + values_.clear(); +} + +template +std::vector MappedAssetManager::getAll() const +{ + std::lock_guard lock{mutex_}; + + std::vector values; + + for (const auto& kv : values_) + { + values.push_back(kv.second); + } + + return values; +} +} diff --git a/core/include/superflow/multi_consumer_port.h b/core/include/superflow/multi_consumer_port.h new file mode 100644 index 0000000..9acf0fa --- /dev/null +++ b/core/include/superflow/multi_consumer_port.h @@ -0,0 +1,231 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/consumer_port.h" +#include "superflow/connection_manager.h" +#include "superflow/policy.h" +#include "superflow/port.h" +#include "superflow/multi_queue_getter.h" +#include "superflow/utils/data_stream.h" +#include "superflow/utils/multi_lock_queue.h" + +#include +#include + +namespace flow +{ +/// \brief The port has one buffer for each connected producer. +/// Data from the the producers are received in a vector with a size depending on the GetMode selected. +/// \tparam T The type of data consumed +/// \tparam M The GetMode, defining the behavior of the port. +/// \see GetMode +/// \tparam Variants... Optionally supported input variant types. \see ConsumerPort +template< + typename T, + GetMode M = GetMode::Blocking, + typename... Variants +> +class MultiConsumerPort final : + public ConsumerPort, + public DataStream>, + public std::enable_shared_from_this +{ +public: + using Ptr = std::shared_ptr; + /// \brief Create a new MultiConsumerPort + /// \param buffer_size Number of elements in each buffer. + explicit MultiConsumerPort( + size_t buffer_size = 1 + ); + + void receive(const T&, const Port::Ptr&) override; + + void connect(const Port::Ptr& ptr) override; + + void disconnect() noexcept override; + + void disconnect(const Port::Ptr& ptr) noexcept override; + + bool isConnected() const override; + + PortStatus getStatus() const override; + + std::optional> getNext() override; + + /// \brief Get new elements from the buffer + /// \return New elements. Number of elements depends on the selected GetMode. + std::vector get(); + + /// \brief Test if any buffer has unconsumed data. + /// \return true if new data is available. + bool hasNext() const; + + operator bool() const override; + + /// \brief Empties the internal buffers, any unread data will be discarded. + void clear(); + + /// \brief Deactivate the port, i.e. terminate all buffers. + void deactivate(); + +private: + size_t num_transactions_ = 0; + + ConnectionManager connection_manager_; + MultiLockQueue multi_queue_; + MultiQueueGetter queue_getter_; +}; + +// ----- Implementation ----- +template< + typename T, + GetMode M, + typename... Variants +> +MultiConsumerPort::MultiConsumerPort( + const size_t buffer_size +) + : multi_queue_{buffer_size} +{} + +template< + typename T, + GetMode M, + typename... Variants +> +inline void MultiConsumerPort::receive(const T& t, const Port::Ptr& ptr) +{ + multi_queue_.push(ptr, t); +} + +template< + typename T, + GetMode M, + typename... Variants +> +void MultiConsumerPort::connect(const Port::Ptr& ptr) +{ + connection_manager_.connect(shared_from_this(), ptr); + multi_queue_.addQueue(ptr); +} + +template< + typename T, + GetMode M, + typename... Variants +> +void MultiConsumerPort::disconnect() noexcept +{ + connection_manager_.disconnect(shared_from_this()); + multi_queue_.removeAllQueues(); +} + +template< + typename T, + GetMode M, + typename... Variants +> +void MultiConsumerPort::disconnect(const Port::Ptr& ptr) noexcept +{ + connection_manager_.disconnect(shared_from_this(), ptr); + multi_queue_.removeQueue(ptr); +} + +template< + typename T, + GetMode M, + typename... Variants +> +bool MultiConsumerPort::isConnected() const +{ + return connection_manager_.getNumConnections() > 0; +} + +template< + typename T, + GetMode M, + typename... Variants +> +PortStatus MultiConsumerPort::getStatus() const +{ + return { + connection_manager_.getNumConnections(), + num_transactions_ + }; +} + +template< + typename T, + GetMode M, + typename... Variants +> +std::optional> MultiConsumerPort::getNext() +{ + try + { + std::vector items; + queue_getter_.get(multi_queue_, items); + + ++num_transactions_; + + return items; + } + catch (const TerminatedException&) + { return std::nullopt; } +} + +template< + typename T, + GetMode M, + typename... Variants +> +std::vector MultiConsumerPort::get() +{ + std::vector items; + queue_getter_.get(multi_queue_, items); + + ++num_transactions_; + + return items; +} + +template< + typename T, + GetMode M, + typename... Variants +> +bool MultiConsumerPort::hasNext() const +{ + return queue_getter_.hasNext(multi_queue_); +} + +template< + typename T, + GetMode M, + typename... Variants +> +MultiConsumerPort::operator bool() const +{ + return !multi_queue_.isTerminated(); +} + +template< + typename T, + GetMode M, + typename... Variants +> +void MultiConsumerPort::clear() +{ + multi_queue_.clear(); +} + +template< + typename T, + GetMode M, + typename... Variants +> +void MultiConsumerPort::deactivate() +{ + multi_queue_.terminate(); +} +} diff --git a/core/include/superflow/multi_queue_getter.h b/core/include/superflow/multi_queue_getter.h new file mode 100644 index 0000000..e4d1161 --- /dev/null +++ b/core/include/superflow/multi_queue_getter.h @@ -0,0 +1,147 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/policy.h" + +#include "superflow/utils/multi_lock_queue.h" + +#include + +namespace flow +{ +template +class MultiQueueGetter +{}; + +template +class MultiQueueGetter +{ +public: + void get(MultiLockQueue& multi_queue, std::vector& items) + { + auto item_map = multi_queue.popAll(); + + items.resize(item_map.size()); + + size_t i = 0; + + for (auto& kv : item_map) + { + items[i++] = std::move(kv.second); + } + } + + bool hasNext(const MultiLockQueue& multi_queue) const + { + return multi_queue.hasAll(); + } +}; + +template +class MultiQueueGetter +{ +public: + void get(MultiLockQueue& multi_queue, std::vector& items) + { + if (last_items_.empty()) + { + last_items_ = multi_queue.popAll(); + } else + { + for (auto& kv : multi_queue.popReady()) + { + last_items_[kv.first] = std::move(kv.second); + } + } + + items.resize(last_items_.size()); + + size_t i = 0; + + for (const auto& kv : last_items_) + { + items[i++] = kv.second; + } + } + + bool hasNext(const MultiLockQueue& multi_queue) const + { + if (last_items_.empty()) + { + return multi_queue.hasAll(); + } + + return true; + } + +private: + std::map last_items_; +}; + +template +class MultiQueueGetter +{ +public: + void get(MultiLockQueue& multi_queue, std::vector& items) + { + auto item_map = multi_queue.popReady(); + items.resize(item_map.size()); + + size_t i = 0; + + for (auto& kv : item_map) + { + items[i++] = std::move(kv.second); + } + } + + bool hasNext(const MultiLockQueue& multi_queue) const + { + return true; + } + +private: + std::map last_items_; +}; + +template +class MultiQueueGetter +{ +public: + void get(MultiLockQueue& multi_queue, std::vector& items) + { + if (last_items_.empty()) + { + last_items_ = multi_queue.popAll(); + } else + { + for (auto& kv : multi_queue.popAtLeastOne()) + { + last_items_[kv.first] = std::move(kv.second); + } + } + + items.resize(last_items_.size()); + + size_t i = 0; + + for (const auto& kv : last_items_) + { + items[i++] = kv.second; + } + } + + bool hasNext(const MultiLockQueue& multi_queue) const + { + if (last_items_.empty()) + { + return multi_queue.hasAll(); + } + + return multi_queue.hasAny(); + } + +private: + std::map last_items_; +}; +} diff --git a/core/include/superflow/multi_requester_port.h b/core/include/superflow/multi_requester_port.h new file mode 100644 index 0000000..d37ddfd --- /dev/null +++ b/core/include/superflow/multi_requester_port.h @@ -0,0 +1,160 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/connection_manager.h" +#include "superflow/mapped_asset_manager.h" +#include "superflow/port.h" +#include "superflow/responder_port.h" + +#include +#include +#include + +namespace flow +{ +template +class MultiRequesterPort; + +/// \brief A MasterPort able to simultaneously request data from several SlavePort +/// \tparam ReturnValue The type av data to request +/// \tparam Args arguments necessary for the SlavePort to respond +template +class MultiRequesterPort final : + public Port, + public std::enable_shared_from_this +{ +public: + using Ptr = std::shared_ptr; + void connect(const Port::Ptr& ptr) override; + + void disconnect() noexcept override; + + void disconnect(const Port::Ptr& ptr) noexcept override; + + PortStatus getStatus() const override; + + bool isConnected() const override; + + /// \brief Request new data from all SlavePort + /// \param args arguments necessary for the SlavePort to respond + /// \return Response from all SlavePort + template + typename std::enable_if_t, std::vector> + request(Args... args); + + template + typename std::enable_if_t> // default er jo void. Flaks! (litt mindre lesbart, dog) + request(Args... args); + + + std::vector> requestAsync(Args... args); + +private: + using Connection = ResponderPort; + using ConnectionPtr = std::shared_ptr; + + size_t num_transactions_ = 0; + ConnectionManager connection_manager_; + MappedAssetManager slaves_; +}; + +// ----- Implementation ----- +template +void MultiRequesterPort::connect(const Port::Ptr& ptr) +{ + auto slave = std::dynamic_pointer_cast>(ptr); + + if (slave == nullptr) + { throw std::invalid_argument{std::string("Type mismatch when connecting ports")}; } + + connection_manager_.connect(shared_from_this(), ptr); + slaves_.put(ptr, slave); +} + +template +void MultiRequesterPort::disconnect() noexcept +{ + connection_manager_.disconnect(shared_from_this()); + slaves_.clear(); +} + +template +void MultiRequesterPort::disconnect(const Port::Ptr& ptr) noexcept +{ + connection_manager_.disconnect(shared_from_this(), ptr); + slaves_.erase(ptr); +} + +template +PortStatus MultiRequesterPort::getStatus() const +{ + return { + connection_manager_.getNumConnections(), + num_transactions_ + }; +} + +template +bool MultiRequesterPort::isConnected() const +{ + return connection_manager_.isConnected(); +} + +template +template +typename std::enable_if_t, std::vector> +MultiRequesterPort::request(Args... args) +{ + const auto slaves = slaves_.getAll(); + + std::vector responses; + responses.reserve(slaves.size()); + + ++num_transactions_; + + for (const auto& slave : slaves) + { + responses.push_back(slave->respond(args...)); + } + + return responses; +} + +template +template +typename std::enable_if_t, void> +MultiRequesterPort::request(Args... args) +{ + const auto slaves = slaves_.getAll(); + + for (const auto& slave : slaves) + { + slave->respond(args...); + } + + ++num_transactions_; +} + +template +inline std::vector> MultiRequesterPort::requestAsync(Args... args) +{ + const auto slaves = slaves_.getAll(); + + std::vector> responses; + responses.reserve(slaves.size()); + + ++num_transactions_; + + for (const auto& slave : slaves) + { + responses.push_back( + std::async( + std::launch::async, + [slave, args...]() { return slave->respond(args...); } + ) + ); + } + + return responses; +} +} diff --git a/core/include/superflow/policy.h b/core/include/superflow/policy.h new file mode 100644 index 0000000..1292c3b --- /dev/null +++ b/core/include/superflow/policy.h @@ -0,0 +1,29 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +namespace flow +{ +enum class GetMode +{ + Blocking, ///< Attempts to retreive data from the buffer is blocking, so that the consumer will + /// wait until new data is added to the buffer. The wait will be aborted only if the + /// consumer is deactivated. + Latched, ///< If buffer is empty, latched mode acts like Blocking. Else, the first data in the + /// buffer will be read. With buffer size 1 you will always get newest data available. + ReadyOnly, ///< When connected to multiple producers one would usually wait for all of them to + /// produce data. This mode fetches only from ready producers, i.e. not necessary all + AtLeastOneNew ///< Similar to Latched, but blocks until at least one of the producers have new data. +}; + +enum class ConnectPolicy +{ + Single, ///< Only allow one ProducerPort to connect. + Multi ///< Allow multiple ProducerPort to connect. +}; + +enum class LeakPolicy +{ + Leaky, ///< Oldest data is dropped when pushing to a full buffer + PushBlocking ///< Push blocks if buffer is full +}; +} diff --git a/core/include/superflow/port.h b/core/include/superflow/port.h new file mode 100644 index 0000000..5429148 --- /dev/null +++ b/core/include/superflow/port.h @@ -0,0 +1,42 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/port_status.h" + +#include + +namespace flow +{ +/// \brief Interface for interconnection between two entities exchanging data. +class Port +{ +public: + using Ptr = std::shared_ptr; + + virtual ~Port() = default; + + /** + * \brief Attempts to connect ports. Does nothing if already connected + * to ptr. Throws if already connected and multiple connections is + * unsupported. + * \throw Exception if connection fails + */ + virtual void connect(const Port::Ptr& ptr) = 0; + + /** + * \brief disconnects port(s) if connected + * otherwise does nothing + */ + virtual void disconnect() noexcept = 0; + + /** + * \brief Disconnects port if connected + * otherwise does nothing + */ + virtual void disconnect(const Port::Ptr& ptr) noexcept = 0; + + [[nodiscard]] virtual bool isConnected() const = 0; + + [[nodiscard]] virtual PortStatus getStatus() const = 0; +}; +} \ No newline at end of file diff --git a/core/include/superflow/port_manager.h b/core/include/superflow/port_manager.h new file mode 100644 index 0000000..5180219 --- /dev/null +++ b/core/include/superflow/port_manager.h @@ -0,0 +1,56 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/port.h" + +#include +#include + +namespace flow +{ +/// \brief Container for managing Ports within a Proxel +/// \see Port, Proxel +class PortManager +{ +public: + /// \brief A map mapping port names to port pointers + using PortMap = std::map; + + /// \brief Create a new PortManager to manage a PortMap + /// \param ports an existing PortMap + explicit PortManager(const PortMap& ports); + + /// \brief Create a PortManager and let the manager own all Ports + /// \param ports an existing PortMap + explicit PortManager(PortMap&& ports); + + ~PortManager(); + + PortManager() = default; + + PortManager(const PortManager&) = default; + + PortManager(PortManager&&) = default; + + PortManager& operator=(const PortManager&) = default; + + PortManager& operator=(PortManager&&) = default; + + /// \brief Get the port with the given name + /// \param name The name of the Port + /// \return A pointer to the Port + [[nodiscard]] const Port::Ptr& get(const std::string& name) const; + + /// \brief Request access to the PortMap managed by the PortManager. + /// \return The PortMap + [[nodiscard]] const PortMap& getPorts() const; + + /// \brief Get the status of all Ports + /// \return A map from port name to port status + /// \see PortStatus + [[nodiscard]] std::map getStatus() const; + +private: + PortMap ports_; +}; +} diff --git a/core/include/superflow/port_status.h b/core/include/superflow/port_status.h new file mode 100644 index 0000000..6a3867d --- /dev/null +++ b/core/include/superflow/port_status.h @@ -0,0 +1,18 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include +#include + +namespace flow +{ +/// \brief A container for statistics and status for a Port +/// \see Port +struct PortStatus +{ + inline static constexpr size_t undefined = std::numeric_limits::max(); + + size_t num_connections; ///< Number of connections to the Port + size_t num_transactions; ///< Number of transactions passed through the Port +}; +} diff --git a/core/include/superflow/producer_port.h b/core/include/superflow/producer_port.h new file mode 100644 index 0000000..891f167 --- /dev/null +++ b/core/include/superflow/producer_port.h @@ -0,0 +1,255 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/consumer_port.h" +#include "superflow/port.h" + +#include +#include +#include + +namespace flow +{ +/// \brief An output port able to connect with multiple ports extending Consumer. +/// \tparam T The type of data to be exchanged between the ports. +/// \tparam Variants... Optional downstream variant types of T which is also to be accepted by +/// the producer. Each such `Variants` must be const ref convertible to `T`, that is +/// `const Variant&` must [be convertible](https://en.cppreference.com/w/cpp/types/is_convertible) to +/// `const T&`. This is also useful for allowing upcasting from a derived class +/// to a parent baseclass. +/// +/// Some examples: +/// ```cpp +/// const auto producer = std::make_shared>(); +/// +/// const auto int_consumer = std::make_shared>(); +/// producer->connect(int_consumer); // OK, producer and consumer types match +/// +/// const auto str_consumer = std::make_shared>(); +/// producer->connect(str_consumer); // NOT OK, int is not std::string, will throw +/// +/// const auto bool_consumer = std::make_shared>(); +/// producer->connect(bool_consumer); // NOT OK, int is not bool, will throw +/// +/// // ---- Advanced usage below ---- +/// +/// const auto conv_producer = std::make_shared>(); // int is implicitly convertible to bool, so this compiles fine +/// conv_producer->connect(bool_consumer); // OK, int is not bool, but bool is registered as a valid conversion for conv_producer +/// +/// struct Base +/// { +/// virtual ~Base() = default; +/// }; +/// +/// struct Derived : public Base +/// {}; +/// +/// const auto base_consumer = std::make_shared>(); +/// const auto derived_producer = std::make_shared>(); // Derived is derived from Base (duh), so this compiles fine +/// derived_producer->connect(base_consumer); // OK, Base is registered as a valid conversion for derived_producer +/// ``` +template +class ProducerPort final : + public Port, + public std::enable_shared_from_this +{ +public: + using Ptr = std::shared_ptr; + /// \brief Send data to all connected consumers. + /// If the consumer reports to not accept data, the connection is removed. + void send(const T&); + + /// \brief Connect port to a new consumer. + /// \param ptr A pointer to the connecting port. + /// \throws std::invalid_argument if ptr is incompatible. + void connect(const Port::Ptr& ptr) override; + + /// \brief Disconnect all consumers. + /// \note Not thread safe! Should not be called while some other thread is sending data. + void disconnect() noexcept override; + + /// \brief Disconnect one consumers. + /// \param ptr A pointer to the other port that will be disconnected + /// \note Not thread safe! Should not be called while some other thread is sending data. + void disconnect(const Port::Ptr& ptr) noexcept override; + + bool isConnected() const override; + + /// \brief Request the number of consumers currently connected. + /// \return + size_t numConnections() const; + + PortStatus getStatus() const override; + +private: + using Consumer = detail::Consumer; + using ConsumerPtr = typename Consumer::Ptr; + using Connection = std::pair; + + template + class ConsumerShim : public detail::Consumer + { + public: + static_assert( + std::is_convertible_v || std::is_constructible_v, + "Cannot create a ProducerPort with support for the given Base-type. const T& is not convertible to const Base&." + ); + + using ConsumerPtr = typename detail::Consumer::Ptr; + + explicit ConsumerShim(const ConsumerPtr& base_consumer) + : base_consumer_{base_consumer} + {} + + void receive(const T& data, const Port::Ptr& port) final + { + if constexpr (std::is_convertible_v) + { + base_consumer_->receive(static_cast(data), port); + } + else + { + // Since const T& is not convertible to const Base&, Base must be + // constructible from const T&. This is less efficient than casting refs. + + base_consumer_->receive(static_cast(data), port); + } + } + + private: + ConsumerPtr base_consumer_; + }; + + /// \brief Helper method for getting a pointer to a Consumer that feeds + /// into a Consumer, from a port. If `T` and `Base` is the same this is + /// a simple cast, otherwise a ConsumerShim is inserted. `consumer` is set to + /// the resulting consumer, or `nullptr` if no such shim is possible. + /// \tparam Base + /// \param ptr + /// \param consumer The resulting consumer, or `nullptr` if no such consumer exists. + /// \return `true` if a valid consumer was successfully found. + template + [[nodiscard]] static bool getConsumerPtr( + const Port::Ptr& ptr, + ConsumerPtr& consumer + ); + + size_t num_transactions_ = 0; + std::map consumers_; + + bool hasConnection(const Port::Ptr& ptr) const; +}; + +// ----- Implementation ----- +template +void ProducerPort::connect(const Port::Ptr& ptr) +{ + ConsumerPtr consumer; + getConsumerPtr(ptr, consumer) || (getConsumerPtr(ptr, consumer) || ...); + + if (consumer == nullptr) + { throw std::invalid_argument{std::string("Type mismatch when connecting ports")}; } + + if (hasConnection(ptr)) + { + // already connected, do nothing + return; + } + + consumers_[ptr] = consumer; + ptr->connect(shared_from_this()); +} + +template +void ProducerPort::disconnect() noexcept +{ + auto consumers = std::move(consumers_); + + for (auto& kv : consumers) + { + kv.first->disconnect(); + } +} + +template +void ProducerPort::disconnect(const Port::Ptr& ptr) noexcept +{ + auto it = consumers_.find(ptr); + + if (it == consumers_.end()) + { + return; + } + + const auto port = it->first; + consumers_.erase(it); + + port->disconnect(shared_from_this()); +} + +template +bool ProducerPort::isConnected() const +{ + return numConnections() > 0; +} + +template +size_t ProducerPort::numConnections() const +{ + return consumers_.size(); +} + +template +PortStatus ProducerPort::getStatus() const +{ + return { + numConnections(), + num_transactions_ + }; +} + +template +void ProducerPort::send(const T& t) +{ + ++num_transactions_; + + for (const auto& kv : consumers_) + { + kv.second->receive(t, shared_from_this()); + } +} + +template +bool ProducerPort::hasConnection(const Port::Ptr& ptr) const +{ + return consumers_.find(ptr) != consumers_.end(); +} + +template +template +bool ProducerPort::getConsumerPtr( + const Port::Ptr& ptr, + ConsumerPtr& consumer +) +{ + const auto base_consumer = std::dynamic_pointer_cast>(ptr); + + if (base_consumer == nullptr) + { + consumer = nullptr; + + return false; + } + + if constexpr (std::is_same_v) + { + consumer = base_consumer; + } + else + { + consumer = std::make_shared>(base_consumer); + } + + return true; +} +} diff --git a/core/include/superflow/proxel.h b/core/include/superflow/proxel.h new file mode 100644 index 0000000..f9f778d --- /dev/null +++ b/core/include/superflow/proxel.h @@ -0,0 +1,70 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/port.h" +#include "superflow/port_manager.h" +#include "superflow/proxel_status.h" +#include "superflow/utils/mutexed.h" + +#include +#include + +namespace flow +{ +/// \brief Abstract class for Processing Element +/// +/// A Proxel, short for processing element, is an isolated "black box" +/// responsible for doing some kind of data manipulation. +/// Proxel is in fact just an interface defining a handful of required methods. +/// Besides that, a class extending the Proxel interface may do whatever +/// the developer decides. The recommended way to create a Proxel is to develop +/// a processing algorithms independently as an external library, +/// and then embed the algorithm into Superflow by wrapping it in a Proxel. +class Proxel +{ +public: + using Ptr = std::shared_ptr; + using ConstPtr = std::shared_ptr; + + virtual ~Proxel() = default; + + /// \brief Method is expected to prepare the Proxel for processing, + /// and make it listen to it's input ports. + virtual void start() = 0; + + /// \brief Method is expected to make the Proxel stop processing and + /// thereby stop producing outputs. If the Proxel is designed to have + /// start() called by an external thread, stop() is expected to make that + /// thread return. + virtual void stop() noexcept = 0; + + /// \brief Request a pointer to a Proxel's port by a given name. + /// \param name The unique name of the port + /// \return + const Port::Ptr& getPort(const std::string& name) const; + + const PortManager::PortMap& getPorts() const; + + /// \brief Request the current status of the Proxel. + /// \return + ProxelStatus getStatus() const; + +protected: + using State = ProxelStatus::State; + + void setState(State state) const; + + void setStatusInfo(const std::string& status_info) const; + + void registerPorts(PortManager::PortMap&& ports); + +private: + mutable std::atomic state_ = State::Undefined; + mutable Mutexed status_info_; + PortManager port_manager_; + + State getState() const; + + std::string getStatusInfo() const; +}; +} diff --git a/core/include/superflow/proxel_config.h b/core/include/superflow/proxel_config.h new file mode 100644 index 0000000..11ce738 --- /dev/null +++ b/core/include/superflow/proxel_config.h @@ -0,0 +1,16 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include +#include + +namespace flow +{ +template +struct ProxelConfig +{ + std::string id; + std::string type; + PropertyList properties; +}; +} diff --git a/core/include/superflow/proxel_status.h b/core/include/superflow/proxel_status.h new file mode 100644 index 0000000..70e809c --- /dev/null +++ b/core/include/superflow/proxel_status.h @@ -0,0 +1,63 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/port_status.h" + +#include +#include +#include + +namespace flow +{ +struct ProxelStatus +{ + enum class State + { + AwaitingInput, + AwaitingRequest, + AwaitingResponse, + Crashed, + NotConnected, + Paused, + Running, + Unavailable, + Undefined, + Warning, + }; + + State state; + std::string info; + std::map ports; +}; + +/// A map with the latest ProxelStatuses, as key/value pairs such as { proxel_name: ProxelStatus } +using ProxelStatusMap = std::map; + +/// \brief Write enum ProxelStatus as string to a std::ostream +inline std::ostream& operator<<(std::ostream& os, const ProxelStatus::State& state) +{ + switch (state) + { + case ProxelStatus::State::AwaitingInput: + return os << "NO INPUT"; + case ProxelStatus::State::AwaitingRequest: + return os << "NO REQUEST"; + case ProxelStatus::State::AwaitingResponse: + return os << "NO RESPONSE"; + case ProxelStatus::State::Crashed: + return os << "CRASHED"; + case ProxelStatus::State::NotConnected: + return os << "NOT CONNECTED"; + case ProxelStatus::State::Paused: + return os << "PAUSED"; + case ProxelStatus::State::Running: + return os << "RUNNING"; + case ProxelStatus::State::Unavailable: + return os << "UNAVAILABLE"; + case ProxelStatus::State::Warning: + return os << "WARNING"; + default: + return os << "UNDEFINED"; + } +} +} diff --git a/core/include/superflow/queue_getter.h b/core/include/superflow/queue_getter.h new file mode 100644 index 0000000..96f4f7e --- /dev/null +++ b/core/include/superflow/queue_getter.h @@ -0,0 +1,65 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/policy.h" +#include "superflow/utils/lock_queue.h" +#include + +namespace flow +{ +template +struct QueueGetter +{ + static_assert(M == GetMode::Blocking || M == GetMode::Latched, "Selected GetMode is not available for this Port"); +}; + +template +struct QueueGetter +{ + static std::optional get(LockQueue& queue) + { + try + { + return queue.pop(); + } + catch (const TerminatedException&) + { return std::nullopt; } + } + + static bool hasNext(const LockQueue& queue) + { + return !queue.isEmpty(); + } + + void clear() + { /* noop */ } +}; + +template +struct QueueGetter +{ + std::optional get(LockQueue& queue) + { + try + { + if (!opt.has_value() || !queue.isEmpty()) + { opt = queue.pop(); } + + return opt; + } + catch (const TerminatedException&) + { return std::nullopt; } + } + + bool hasNext(const LockQueue& queue) const + { return opt.has_value() || !queue.isEmpty(); } + + void clear() + { + opt = std::nullopt; + } + +private: + std::optional opt; +}; +} diff --git a/core/include/superflow/queue_set_getter.h b/core/include/superflow/queue_set_getter.h new file mode 100644 index 0000000..205137b --- /dev/null +++ b/core/include/superflow/queue_set_getter.h @@ -0,0 +1,82 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/policy.h" +#include "superflow/queue_getter.h" +#include "superflow/utils/lock_queue.h" + +#include +#include + +namespace flow +{ +template +struct QueueSetGetter +{ + using Queue = LockQueue; + using QueueSet = std::vector>; + + static void get(const QueueSet& queues, std::vector& ts) + { + ts.resize(queues.size()); + + for (size_t i = 0; i < queues.size(); ++i) + { + QueueGetter::get(*queues[i], ts[i]); + } + } + + static bool hasNext(const QueueSet& queues) + { + for (const auto& queue : queues) + { + if (queue->getQueueSize() == 0) + { + return false; + } + } + + return true; + } +}; + +template +struct QueueSetGetter +{ + using Queue = LockQueue; + using QueueSet = std::vector>; + + static void get(const QueueSet& queues, std::vector& ts) + { + QueueSet ready_queues; + + for (const auto& queue : queues) + { + if (queue->getQueueSize() > 0) + { + ready_queues.push_back(queue); + } + } + + ts.resize(ready_queues.size()); + + for (size_t i = 0; i < ready_queues.size(); ++i) + { + QueueGetter::get(*ready_queues[i], ts[i]); + } + } + + static bool hasNext(const QueueSet& queues) + { + for (const auto& queue : queues) + { + if (queue->getQueueSize() > 0) + { + return true; + } + } + + return false; + } +}; +} diff --git a/core/include/superflow/requester_port.h b/core/include/superflow/requester_port.h new file mode 100644 index 0000000..74be479 --- /dev/null +++ b/core/include/superflow/requester_port.h @@ -0,0 +1,201 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/port.h" +#include "superflow/responder_port.h" + +#include +#include +#include + +namespace flow +{ +template +class RequesterPort; + +/// \brief A port type that can request a response from a connected ResponderPort +/// \tparam ReturnValue The type of response required +/// \tparam Args Any arguments required by the slave to produce the response +/// \tparam Variants Optional variant types of `ReturnValue` that are also to be accepted by +/// the responder. `ReturnValue` must be convertible to each such `Variants`. That is, either `ReturnValue` +/// must [be convertible](https://en.cppreference.com/w/cpp/types/is_convertible) to `Variants`, or `Variants` +/// must [be constructible](https://en.cppreference.com/w/cpp/types/is_constructible) from `ReturnValue`. This is +/// also useful for allowing upcasting from a derived `ReturnValue` to a parent `Variant`. +/// \see ResponderPort +/// \see \ref ResponderPort "flow::ResponderPort" +template +class RequesterPort : + public Port, + public std::enable_shared_from_this +{ +public: + using Ptr = std::shared_ptr; + ~RequesterPort() override = default; + + void connect(const Port::Ptr& ptr) final; + + void disconnect() noexcept final; + + void disconnect(const Port::Ptr& ptr) noexcept final; + + PortStatus getStatus() const final; + + bool isConnected() const final; + + /// \brief Request a new response from the slave + /// \param args Any arguments required by the slave to produce the response + /// \return The response + ReturnValue request(Args... args) + { + if (responder_) + { + ++num_transactions_; + + return responder_->respond(args..., nullptr); + } else + { throw std::runtime_error("RequesterPort has no connection"); } + } + + /// \brief Overload for request + /// \param args Any arguments required by the slave to produce the response + /// \return The response + ReturnValue operator()(Args... args) + { return request(args...); } + +private: + using Responder = detail::Responder; + using ResponderPtr = typename Responder::Ptr; + + size_t num_transactions_ = 0; + + Port::Ptr connection_; + ResponderPtr responder_; + + template + class ResponderShim : public detail::Responder + { + public: + static_assert( + std::is_convertible_v || std::is_constructible_v, + "Cannot create a RequesterPortPort with support for the given Variant-type. Variant is not convertible to ReturnValue." + ); + + using ResponderPtr = typename detail::Responder::Ptr; + + explicit ResponderShim(const ResponderPtr& variant_responder) + : variant_responder_{variant_responder} + {} + + ReturnValue respond(Args... args, const ReturnValue*) final + { + return static_cast(variant_responder_->respond(args..., nullptr)); + } + + private: + ResponderPtr variant_responder_; + }; + + template + [[nodiscard]] static bool getResponderPtr( + const Port::Ptr& ptr, + ResponderPtr& responder + ); +}; + +/// \brief Partial template specialization for the case when `ReturnValue` is a [`std::variant`](https://en.cppreference.com/w/cpp/utility/variant). +/// For further details, \see RequesterPort. +template +class RequesterPort(Args...)> : + public RequesterPort(Args...), Variants...> +{}; + +// ----- Implementation ----- +template +void RequesterPort::connect(const Port::Ptr& ptr) +{ + if (connection_ == ptr) + { + return; + } + + ResponderPtr responder; + getResponderPtr(ptr, responder) || (getResponderPtr(ptr, responder) || ...); + + if (responder == nullptr) + { throw std::invalid_argument{std::string("Type mismatch when connecting ports")}; } + + if (isConnected()) + { throw std::runtime_error{"The RequesterPort has already an active connection."}; } + + responder_ = responder; + connection_ = ptr; + ptr->connect(shared_from_this()); +} + +template +void RequesterPort::disconnect() noexcept +{ + if (connection_ == nullptr) + { + return; + } + + responder_.reset(); + auto connection = std::move(connection_); + connection->disconnect(); +} + +template +void RequesterPort::disconnect(const Port::Ptr& ptr) noexcept +{ + if (connection_ != ptr) + { + return; + } + + disconnect(); +} + +template +PortStatus RequesterPort::getStatus() const +{ + return { + isConnected() ? 1u : 0u, + num_transactions_ + }; +} + +template +bool RequesterPort::isConnected() const +{ + return connection_ != nullptr; +} + +template +template +bool RequesterPort::getResponderPtr( + const Port::Ptr& ptr, + ResponderPtr& responder +) +{ + const auto variant_responder = std::dynamic_pointer_cast>(ptr); + + if (variant_responder == nullptr) + { + responder = nullptr; + + return false; + } + + if constexpr (std::is_same_v) + { + responder = variant_responder; + } + else + { + responder = std::make_shared>(variant_responder); + } + + return true; +} +} diff --git a/core/include/superflow/responder_port.h b/core/include/superflow/responder_port.h new file mode 100644 index 0000000..8b9c13f --- /dev/null +++ b/core/include/superflow/responder_port.h @@ -0,0 +1,203 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/connection_manager.h" +#include "superflow/policy.h" +#include "superflow/port.h" + +#include +#include +#include + +namespace flow +{ +namespace detail +{ +template +class Responder +{ +public: + using Ptr = std::shared_ptr; + + virtual ~Responder() = default; + + virtual ReturnValue respond(Args... args, const ReturnValue*) = 0; +}; + +template +class ResponderVariant + : protected virtual Responder + , public Responder +{ +public: + static_assert( + std::is_convertible_v || std::is_constructible_v, + "Cannot create a ResponderVariant with the Variant and ReturnValue types specified in your ResponderPort because ReturnValue is not convertible to Variant." + ); + + Variant respond(Args... args, const Variant*) override + { + const auto result = static_cast&>(*this).respond(args..., nullptr); + + return static_cast(result); + } +}; +} + +template +class ResponderPort; + +/// \brief A port type that receives requests from a connected RequesterPort +/// and returns a response to that request +/// \tparam ReturnValue The type of response required +/// \tparam Args Any arguments required to produce the response +/// \tparam Variants Optional variant types of `ReturnValue` that are also to be accepted by +/// the responder. `ReturnValue` must be convertible to each such `Variants`. That is, either `ReturnValue` +/// must [be convertible](https://en.cppreference.com/w/cpp/types/is_convertible) to `Variants`, or `Variants` +/// must [be constructible](https://en.cppreference.com/w/cpp/types/is_constructible) from `ReturnValue`. This is +/// also useful for allowing upcasting from a derived `ReturnValue` to a parent `Variant`. +/// \see RequesterPort +/// \see \ref RequesterPort "flow::RequesterPort" +/// +/// Some examples: +/// ```cpp +/// const auto responder = std::make_shared>( +/// [](const int exponent, const double base) +/// { +/// return 10*std::pow(base, static_cast(exponent)); +/// } +/// ); +/// +/// const auto requester = std::make_shared>(); +/// requester->connect(responder); // OK, requester and responder types match +/// +/// const auto string_requester = std::make_shared>(); +/// string_requester->connect(responder); // NOT OK, string_requester expects std::string return type +/// +/// const auto float_requester = std::make_shared>(); +/// float_requester->connect(responder); // NOT OK, float_requester expects float return type +/// +/// // ---- Advanced usage below ---- +/// +/// // here we make a responder which returns int, but says that it is also OK to convert this int to a bool +/// const auto responder = std::make_shared>( // int is implicitly convertible to bool, so this compiles fine +/// [](const int val) { return 42*val; } +/// ); +/// const auto bool_requester = std::make_shared>(); +/// bool_requester->connect(responder); // OK, int is not bool, but bool is registered as a valid conversion for responder +/// +/// struct Base +/// { +/// virtual ~Base() = default; +/// }; +/// +/// struct Derived : public Base +/// {}; +/// +/// const auto derived_responder = std::make_shared>( +/// []() { return Derived{}; } +/// ); +/// +/// // here we make a requester which expects a Base to be returned from the responder, but which will also +/// // accept converting (up-casting in this case) a response of type Derived +/// const auto base_requester = std::make_shared>(); // Derived is derived from Base (duh), so this compiles fine +/// base_requester->connect(derived_responder); // OK, Base is registered as a valid conversion for base_requester +/// ``` +template +class ResponderPort final : + public Port, + public virtual detail::Responder, + public detail::ResponderVariant..., + public std::enable_shared_from_this +{ +public: + using Ptr = std::shared_ptr; + + /// \brief Create a new SlavePort + /// \param callback The function to be called as a response to requests + explicit ResponderPort(std::function callback) + : callback_{callback} + {} + + ~ResponderPort() override = default; + + void connect(const Port::Ptr& ptr) override; + + void disconnect() noexcept override; + + void disconnect(const Port::Ptr& ptr) noexcept override; + + bool isConnected() const override; + + PortStatus getStatus() const override; + + /// \brief Respond to a request from a MasterPort + /// \param args Any arguments required by the slave to produce the response + /// \return The response + ReturnValue respond(Args... args); + + ReturnValue respond(Args... args, const ReturnValue*) override; + +private: + ConnectionManager connection_manager_; + size_t num_transactions_ = 0; + std::function callback_; +}; + +// ----- Implementation ----- +template +void ResponderPort::connect(const Port::Ptr& ptr) +{ + connection_manager_.connect(shared_from_this(), ptr); +} + +template +bool ResponderPort::isConnected() const +{ + return connection_manager_.isConnected(); +} + +template +void ResponderPort::disconnect() noexcept +{ + connection_manager_.disconnect(shared_from_this()); +} + +template +void ResponderPort::disconnect(const Port::Ptr& ptr) noexcept +{ + connection_manager_.disconnect(shared_from_this(), ptr); +} + +template +PortStatus ResponderPort::getStatus() const +{ + return { + connection_manager_.getNumConnections(), + num_transactions_ + }; +} + +template +ReturnValue ResponderPort::respond(Args... args) +{ + return respond(args..., nullptr); +} + +template +ReturnValue ResponderPort::respond(Args... args, const ReturnValue*) +{ + if constexpr (std::is_same_v) + { + callback_(args...); + ++num_transactions_; + } + else + { + const auto value = callback_(args...); + ++num_transactions_; + + return value; + } +} +} diff --git a/core/include/superflow/utils/blocker.h b/core/include/superflow/utils/blocker.h new file mode 100644 index 0000000..9e8ec10 --- /dev/null +++ b/core/include/superflow/utils/blocker.h @@ -0,0 +1,75 @@ +// Copyright (c) 2022 Forsvarets forskningsinstitutt (FFI). All rights reserved. + +#pragma once + +#include +#include +#include +#include +#include + +namespace flow +{ +/// \brief Class for controlling flow when doing multi threaded testing. Blocker::block() blocks the current thread until +/// Blocker::release() is called in another thread. This can be used to make the main thread wait for some computation in +/// another thread. +struct Blocker +{ + /// \brief Block the current thread until another thread calls release() + /// \return return false if the thread was already released so no blocking was necessary, + /// or true if an actual block was experienced. + bool block() + { + if (is_released_) + { return false; } // did not wait + + std::unique_lock lock{mutex_}; + cv_.wait(lock, [this] + { return static_cast(is_released_); } + ); + + return true; + } + + /// \brief Unblock the thread(s) that has called block() + void release() + { + is_released_ = true; + cv_.notify_all(); + } + + /// \brief Resets the Blocker. Further calls to block() will block until someone again calls release() + void rearm() + { is_released_ = false; } + + bool isReleased() const + { return is_released_; } + +private: + mutable std::mutex mutex_{}; + mutable std::condition_variable cv_; + std::atomic_bool is_released_{false}; +}; + +/// \brief Function that calls Blocker::release() whenever the thread exits. Useful when testing uncontrolled exits from +/// the thread, e.g exceptions. +inline void unblockOnThreadExit(Blocker& blocker) +{ + class Unblocker + { + public: + explicit Unblocker(Blocker& blocker) : blocker_{blocker} + {} + + ~Unblocker() + { + blocker_.release(); + } + + private: + Blocker& blocker_; + }; + + thread_local Unblocker unblocker(blocker); +} +} diff --git a/core/include/superflow/utils/data_stream.h b/core/include/superflow/utils/data_stream.h new file mode 100644 index 0000000..3def823 --- /dev/null +++ b/core/include/superflow/utils/data_stream.h @@ -0,0 +1,118 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include +#include +#include + +namespace flow +{ +/// \brief Interface for classes that will continuosly produce data. +/// Defines a convenient extract stream operator for those classes, +/// facilitating statements like `for(T item; stream >> item;){}` +/// \tparam T +template +class DataStream +{ +public: + virtual ~DataStream() = default; + + /// \brief Request the next item from the stream. + /// \param item The element that will contain the newly retreived data. + /// \return Should return true if item contains valid data. + virtual std::optional getNext() = 0; + + /// \brief Tells whether the stream is valid, i.e. is alive and produces valid data. + /// \return Should return true if the stream is valid. + virtual operator bool() const = 0; + + class Iterator + { + public: + using iterator_category = std::input_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = T*; + using reference = T&; + + explicit Iterator(DataStream& stream) //NOLINT + : Iterator{stream, !stream} + {} + + Iterator(DataStream& stream, const bool is_end) + : stream_{stream} + , is_end_{is_end} + { + ++*this; + } + + Iterator& operator++() + { + if (!is_end_) + { + t_ = stream_.getNext(); + is_end_ = not t_.has_value(); + } + + return *this; + } + + Iterator operator++(int) + { + auto retval = *this; + + ++(*this); + + return retval; + } + + bool operator==(const Iterator& other) const + { + if (is_end_ || !stream_) + { return other.is_end_ || !other.stream_; } + else + { return !other.is_end_ && other.stream_; } + } + + bool operator!=(const Iterator& other) const + { + return !(*this == other); + } + + constexpr const T& operator*() const & { return *t_; } + + T& operator*() & { return *t_; } + + T&& operator*() && { return std::move(*t_); } + + private: + DataStream& stream_; + bool is_end_; + std::optional t_; + }; + + Iterator begin() + { + return Iterator{*this}; + } + + Iterator end() + { + return {*this, true}; + } +}; + +/// \brief Extract operator for DataStream +/// \tparam T the type of data in the stream. +/// \param stream the stream. +/// \param item element containing the received data +/// \return the stream. +template +inline DataStream& operator>>(DataStream& stream, T& item) +{ + if (auto data = stream.getNext()) + { item = std::move(*data); } + + return stream; +} +} diff --git a/core/include/superflow/utils/graphviz.h b/core/include/superflow/utils/graphviz.h new file mode 100644 index 0000000..073c46a --- /dev/null +++ b/core/include/superflow/utils/graphviz.h @@ -0,0 +1,81 @@ +#pragma once + +#include "superflow/connection_spec.h" + +#include +#include +#include +#include + +template<> +struct std::hash +{ + std::size_t operator()(const flow::ConnectionSpec& c) const noexcept + { + std::size_t h1 = std::hash{}(c.lhs_name); + std::size_t h2 = std::hash{}(c.lhs_port); + std::size_t h3 = std::hash{}(c.rhs_name); + std::size_t h4 = std::hash{}(c.rhs_port); + return h1 ^ (h2 << 1) ^ (h3 << 2) ^ (h4 << 3); + } +}; + +template<> +struct std::equal_to +{ + bool operator()( + const flow::ConnectionSpec& lhs, + const flow::ConnectionSpec& rhs + ) const + { + return + lhs.lhs_name == rhs.lhs_name && + lhs.lhs_port == rhs.lhs_port && + lhs.rhs_name == rhs.rhs_name && + lhs.rhs_port == rhs.rhs_port; + } +}; + +namespace flow +{ +/// \brief Parse ConnectionSpec%s to create graphviz source code for rendering with dot +/// +/// \code{.cpp} +/// std::ofstream{"./superflow.gv"} << flow::yaml::generateDOTFile(config_file_path) << std::endl; +/// \endcode +/// +/// \code{.sh} +/// dot -Tsvg -o graph.svg superflow.gv +/// \endcode +class GraphViz +{ +public: + /// Build the internal data structure parsed from the ConnectionSpec%s. + /// + /// We also support dummy ConnectionSpec%s like {proxel_name,{},{},{}}, + /// so that unconnected proxels can also be rendered. + /// \param connections + GraphViz( + const std::vector& connections + ); + + /// Render the DOT-file source code + std::string employ() const; + +private: + using AdjacencyList = std::unordered_set; + + struct ProxelMeta + { + AdjacencyList adjacency_list; + std::set lhs_ports; + std::set rhs_ports; + }; + + std::map node_list; + + void insert(const ConnectionSpec& connection); + std::string getNodeDefinitions() const; + std::string getNodeConnections() const; +}; +} diff --git a/core/include/superflow/utils/lock_queue.h b/core/include/superflow/utils/lock_queue.h new file mode 100644 index 0000000..e2623d9 --- /dev/null +++ b/core/include/superflow/utils/lock_queue.h @@ -0,0 +1,231 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/policy.h" +#include "superflow/utils/terminated_exception.h" + +#include +#include +#include + +namespace flow +{ +template +class LockQueue +{ +public: + explicit LockQueue(unsigned int max_queue_size); + + LockQueue(unsigned int max_queue_size, std::initializer_list list); + + ~LockQueue(); + + void clearQueue(); + + [[nodiscard]] size_t getQueueSize() const; + + [[nodiscard]] bool isEmpty() const; + + [[nodiscard]] bool isTerminated() const; + + void terminate(); + + void push(const T& item); + + void push(T&& item); + + void front(T& t) const; + + [[nodiscard]] T front() const; + + T pop(); + + void pop(T&); + +private: + mutable std::mutex mutex_; + mutable std::condition_variable consumer_; + mutable std::condition_variable producer_; + + std::queue queue_; + const unsigned long max_queue_size_ = 1; + bool terminated_ = false; + + std::unique_lock consumerWait() const; + + std::unique_lock producerWait() const; + + void consumerSatisfied() const; + + void producerSatisfied() const; +}; + +// ----- Implementations +template +LockQueue::LockQueue(unsigned int max_queue_size) + : LockQueue(max_queue_size, {}) +{} + +template +LockQueue::LockQueue(unsigned int max_queue_size, std::initializer_list list) + : queue_{list} + , max_queue_size_(max_queue_size) +{ + if (max_queue_size_ < 1) + { throw std::invalid_argument("LockQueue ctor: argument 'max_queue_size' must be 1 or more."); } + + if (queue_.size() > max_queue_size) + { throw std::range_error("initializer list contains more than 'max_queue_size' elements"); } +} + +template +LockQueue::~LockQueue() +{ terminate(); } + +template +void LockQueue::clearQueue() +{ + std::lock_guard mlock(mutex_); + std::queue empty; + std::swap(queue_, empty); +} + +template +size_t LockQueue::getQueueSize() const +{ + std::lock_guard lock{mutex_}; + return queue_.size(); +} + +template +bool LockQueue::isEmpty() const +{ + std::lock_guard lock{mutex_}; + return queue_.empty(); +} + +template +bool LockQueue::isTerminated() const +{ return terminated_; } + +template +void LockQueue::terminate() +{ + if (terminated_) + { return; } + + terminated_ = true; + producer_.notify_all(); + consumer_.notify_all(); +} + +template +void LockQueue::front(T& t) const +{ + const auto mlock = consumerWait(); + + t = queue_.front(); +} + +template +T LockQueue::front() const +{ + const auto mlock = consumerWait(); + + return queue_.front(); +} + +template +T LockQueue::pop() +{ + auto mlock = consumerWait(); + + T item = std::move(queue_.front()); + queue_.pop(); + + mlock.unlock(); + consumerSatisfied(); + return item; +} + +template +void LockQueue::pop(T& item) +{ + auto mlock = consumerWait(); + + std::swap(item, queue_.front()); + queue_.pop(); + mlock.unlock(); + consumerSatisfied(); +} + +template +void LockQueue::push(T&& item) +{ + auto mlock = producerWait(); + + if (queue_.size() >= max_queue_size_) + { queue_.pop(); } + + queue_.push(std::move(item)); + mlock.unlock(); + producerSatisfied(); +} + +template +void LockQueue::push(const T& item) +{ + auto mlock = producerWait(); + + if (queue_.size() >= max_queue_size_) + { queue_.pop(); } + + queue_.push(item); + mlock.unlock(); + producerSatisfied(); +} + +template +std::unique_lock LockQueue::consumerWait() const +{ + std::unique_lock mlock(mutex_); + consumer_.wait(mlock, [this](){ return !queue_.empty() || terminated_; }); + + if (terminated_) + { throw TerminatedException(); } + + return mlock; +} + +template +std::unique_lock LockQueue::producerWait() const +{ + std::unique_lock mlock(mutex_); + + if constexpr (L == LeakPolicy::PushBlocking) + { + producer_.wait(mlock, [this]() + { return queue_.size() < max_queue_size_ || terminated_; }); + } + + if (terminated_) + { throw TerminatedException(); } + + return mlock; +} + +template +void LockQueue::consumerSatisfied() const +{ + if constexpr (L == LeakPolicy::PushBlocking) + { producer_.notify_one(); } + + consumer_.notify_one(); +} + +template +void LockQueue::producerSatisfied() const +{ + consumer_.notify_one(); +} +} diff --git a/core/include/superflow/utils/metronome.h b/core/include/superflow/utils/metronome.h new file mode 100644 index 0000000..434f509 --- /dev/null +++ b/core/include/superflow/utils/metronome.h @@ -0,0 +1,68 @@ +// Copyright 2020, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include +#include +#include + +namespace flow +{ +/// \brief Calls a provided function on a separate thread at a given +/// interval, omitting the first (immediate) call. Will continue to +/// do so forever, until `stop()` or dtor is called. Useful for instance for +/// printing error messages for stalling tasks. +/// +/// ```cpp +/// Metronome repeater{ +/// [](const auto& duration) +/// { +/// std::cerr +/// << "my_func has been stalling for " << duration.count() << "s" +/// << std::endl; +/// }, +/// std::chrono::seconds{2} +/// }; +/// +/// my_func(); // we expect that this might stall +/// repeater.stop(); +/// +/// // `repeater` will print the above message every 2 seconds until +/// // `my_func()` has returned and `stop()` has been called. +/// ``` +/// +/// If `my_func()` throws an exception, the repeater will stop, +/// and a subsequent call to `check()` will rethrow the exception. +/// +/// \see Sleeper, Throttle +class Metronome +{ +public: + using Duration = std::chrono::steady_clock::duration; + using Functional = std::function; + + Metronome( + const Functional& func, + Duration period + ); + + ~Metronome(); + + /// Wait for the worker to finish, and also receive any exceptions + /// thrown in the worker thread (inside `func`). + void get(); + + /// Method for testing if `func` has thrown an exception. + /// Re-throws the exception thrown by `func`, if it has thrown. + /// Otherwise returns immediately. + void check(); + + /// Stop calling the provided `func` (and unblock dtor) + void stop() noexcept; + +private: + bool has_stopped_; + std::mutex mutex_; + std::condition_variable cv_; + std::future worker_; +}; +} \ No newline at end of file diff --git a/core/include/superflow/utils/multi_lock_queue.h b/core/include/superflow/utils/multi_lock_queue.h new file mode 100644 index 0000000..fe2247a --- /dev/null +++ b/core/include/superflow/utils/multi_lock_queue.h @@ -0,0 +1,467 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/utils/terminated_exception.h" + +#include +#include +#include +#include +#include + +namespace flow +{ +template +class MultiLockQueue +{ +public: + /// Creates a new MultiLockQueue where each queue has a max size of `max_queue_size`. + /// No queues are initialized by the ctor, but will be added dynamically as you call + /// `push()` for unused keys, or by using `addQueue()`. + explicit MultiLockQueue( + size_t max_queue_size + ); + + /// Creates a new MultiLockQueue where each queue has a max size of `max_queue_size`. + /// Queues for each entry in `keys` are added by the ctor. Additional queues will be + /// added dynamically as you call `push()` for unused keys, or by using `addQueue()`. + MultiLockQueue( + size_t max_queue_size, + const std::vector& keys + ); + + void push(const K& key, const T& item); + + void push(const K& key, T&& item); + + /// Returns a map of the first item in all non-empty queues, + /// while not removing the elements. Does not block. Ever. + /// If all queues are empty, the returned map will be empty. + /// \throws TerminatedException if terminate() has been called + /// prior to calling peekReady() + std::map peekReady() const; + + /// Returns a map of the first item in all non-empty queues, + /// while not removing the elements. Blocks until at least one + /// of the queues has an element. The returned map will thus + /// contain at least one element. + /// \throws TerminatedException if terminate() is called + /// prior to calling peekAtLeastOne() or while waiting for data. + std::map peekAtLeastOne() const; + + /// Returns a map of the first item in all queues, + /// while not removing the elements. Blocks until all queues + /// have an element. The returned map will thus contain as many + /// elements as there are queues. + /// \throws TerminatedException if terminate() is called + /// prior to calling peekAll() or while waiting for data. + std::map peekAll() const; + + /// Returns a map of the first item in all non-empty queues, + /// and removes the elements from the queues. Does not block. + /// Ever. If all queues are empty, the returned map will be + /// empty. + /// \throws TerminatedException if terminate() has been called + /// prior to calling popReady() + std::map popReady(); + + /// Returns a map of the first item in all non-empty queues, + /// and removes the elements from the queues. Blocks until at + /// least one of the queues has an element. The returned map + /// will thus contain at least one element. Blocks indefinitely + /// if there are no queues. + /// \throws TerminatedException if terminate() is called + /// prior to calling popAtLeastOne() or while waiting for data. + std::map popAtLeastOne(); + + /// Returns a map of the first item in all queues, + /// and removes the elements from the queues. Blocks until + /// all queues have an element. The returned map will thus + /// contain as many elements as there are queues. + /// \throws TerminatedException if terminate() is called + /// prior to calling popAll() or while waiting for data. + std::map popAll(); + + void clear(); + + /// Adds a new queue for `key`. Does nothing if a queue for + /// this key already exists. + void addQueue(const K& key); + + /// Removes the queue for `key`. Does nothing if no such queue + /// exists + void removeQueue(const K& key); + + void removeAllQueues(); + + /// Returns true if at least one of the queues has at least one + /// element. Does not block. Returns false if there are no queues. + /// If it returns `true`, popAtLeastOne and peekAtLeastOne are guaranteed + /// to return immediately. + bool hasAny() const; + + /// Returns true if all queues have at least one element. + /// Returns true if there are no queues. + /// If it returns `true`, all pop- and peek-methods are guaranteed + /// to return immediately. + bool hasAll() const; + + /// Sets the terminated flag and causes all + /// calls to pop- and peek-methods to throw TerminatedException. + /// This also happens to preexisting calls that are blocking while + /// waiting for data. + void terminate(); + + /// Returns true if queue has been terminated. When true + /// all calls to pop- and peek-methods will throw TerminatedException. + bool isTerminated() const; + + /// Returns the number of queues. + size_t getNumQueues() const; + +private: + mutable std::mutex mutex_; + mutable std::condition_variable cond_; + + std::map> queues_; + size_t max_queue_size_; + bool terminated_; + + std::unique_lock waitAny() const; + + std::unique_lock waitAll() const; + + std::map peekReady(const std::unique_lock& lock) const; + + std::map popReady(const std::unique_lock& lock); + + bool hasAny(const std::unique_lock& lock) const; + + bool hasAll(const std::unique_lock& lock) const; + + static std::map> createQueues(const std::vector& keys); +}; + +// ----- Implementation ----- +template +MultiLockQueue::MultiLockQueue(const size_t max_queue_size) + : MultiLockQueue{max_queue_size, {}} +{} + +template +MultiLockQueue::MultiLockQueue( + const size_t max_queue_size, + const std::vector& keys +) + : queues_{createQueues(keys)} + , max_queue_size_{max_queue_size} + , terminated_{false} +{} + +template +void MultiLockQueue::push(const K& key, const T& item) +{ + { + std::lock_guard lock{mutex_}; + + auto& queue = queues_[key]; + + if (queue.size() >= max_queue_size_) + { + queue.pop(); + } + + queue.push(item); + } + + cond_.notify_one(); +} + +template +void MultiLockQueue::push(const K& key, T&& item) +{ + { + std::lock_guard lock{mutex_}; + + auto& queue = queues_[key]; + + if (queue.size() >= max_queue_size_) + { + queue.pop(); + } + + queue.push(std::move(item)); + } + + cond_.notify_one(); +} + +template +std::map MultiLockQueue::peekReady() const +{ + if (terminated_) + { + throw TerminatedException(); + } + + const std::unique_lock lock{mutex_}; + + return peekReady(lock); +} + +template +std::map MultiLockQueue::peekAtLeastOne() const +{ + const auto& lock = waitAny(); + + return peekReady(lock); +} + +template +std::map MultiLockQueue::peekAll() const +{ + const auto& lock = waitAll(); + + return peekReady(lock); +} + +template +std::map MultiLockQueue::popReady() +{ + if (terminated_) + { + throw TerminatedException(); + } + + const std::unique_lock lock{mutex_}; + + return popReady(lock); +} + +template +std::map MultiLockQueue::popAtLeastOne() +{ + const auto& lock = waitAny(); + + return popReady(lock); +} + +template +std::map MultiLockQueue::popAll() +{ + const auto& lock = waitAll(); + + return popReady(lock); +} + +template +void MultiLockQueue::clear() +{ + std::lock_guard lock{mutex_}; + + for (auto& kv : queues_) + { + auto& queue = kv.second; + + std::queue empty_queue; + std::swap(queue, empty_queue); + } +} + +template +void MultiLockQueue::addQueue(const K& key) +{ + std::lock_guard lock{mutex_}; + + if (queues_.find(key) != queues_.end()) + { + return; + } + + queues_[key] = std::queue{}; +} + +template +void MultiLockQueue::removeQueue(const K& key) +{ + std::lock_guard lock{mutex_}; + + queues_.erase(key); +} + +template +void MultiLockQueue::removeAllQueues() +{ + std::lock_guard lock{mutex_}; + + queues_.clear(); +} + +template +bool MultiLockQueue::hasAny() const +{ + const std::unique_lock lock{mutex_}; + + return hasAny(lock); +} + +template +bool MultiLockQueue::hasAny(const std::unique_lock&) const +{ + for (const auto& kv : queues_) + { + const auto& queue = kv.second; + + if (!queue.empty()) + { + return true; + } + } + + return false; +} + +template +bool MultiLockQueue::hasAll() const +{ + const std::unique_lock lock{mutex_}; + + return hasAll(lock); +} + +template +bool MultiLockQueue::hasAll(const std::unique_lock&) const +{ + for (const auto& kv : queues_) + { + const auto& queue = kv.second; + + if (queue.empty()) + { + return false; + } + } + + return true; +} + +template +void MultiLockQueue::terminate() +{ + if (terminated_) + { + return; + } + + terminated_ = true; + cond_.notify_all(); +} + +template +bool MultiLockQueue::isTerminated() const +{ + return terminated_; +} + +template +size_t MultiLockQueue::getNumQueues() const +{ + std::lock_guard lock{mutex_}; + + return queues_.size(); +} + +template +std::unique_lock MultiLockQueue::waitAny() const +{ + std::unique_lock lock{mutex_}; + + cond_.wait( + lock, + [this, &lock]() + { + return terminated_ || hasAny(lock); + } + ); + + if (terminated_) + { + throw TerminatedException(); + } + + return lock; +} + +template +std::unique_lock MultiLockQueue::waitAll() const +{ + std::unique_lock lock{mutex_}; + + cond_.wait( + lock, + [this, &lock]() + { + return terminated_ || hasAll(lock); + } + ); + + if (terminated_) + { + throw TerminatedException(); + } + + return lock; +} + +template +std::map MultiLockQueue::peekReady(const std::unique_lock&) const +{ + std::map items; + + for (const auto& kv : queues_) + { + const auto& key = kv.first; + const auto& queue = kv.second; + + if (!queue.empty()) + { + items[key] = queue.front(); + } + } + + return items; +} + +template +std::map MultiLockQueue::popReady(const std::unique_lock&) +{ + std::map items; + + for (auto& kv : queues_) + { + const auto& key = kv.first; + auto& queue = kv.second; + + if (!queue.empty()) + { + items[key] = queue.front(); + queue.pop(); + } + } + + return items; +} + +template +std::map> MultiLockQueue::createQueues(const std::vector& keys) +{ + std::map> queues; + + for (const auto& key : keys) + { + queues[key] = std::queue{}; + } + + return queues; +} +} diff --git a/core/include/superflow/utils/mutexed.h b/core/include/superflow/utils/mutexed.h new file mode 100644 index 0000000..3693aef --- /dev/null +++ b/core/include/superflow/utils/mutexed.h @@ -0,0 +1,126 @@ +// Copyright (c) 2019 Forsvarets forskningsinstitutt (FFI). All rights reserved. +#pragma once + +#include +#include + +namespace flow +{ +/// \brief A wrapper class for protecting an object with a mutex. +/// \note Note that since Mutexed is derived from T, T cannot be pointer or refrence. +/// +/// \code{.cpp} +/// Mutexed mutexed("hello"); +/// { +/// std::scoped_lock lock{mutexed}; +/// mutexed = "hello"; +/// } +/// +/// mutexed.store("bye"); +/// std::string bye = mutexed.load(); +/// +/// mutexed.read([&bye](auto const& str){ bye = str; } +/// mutexed.write([](auto& str){ str = "hello"; } +/// \endcode +/// \tparam T The type of object you want to protect. +/// \see http://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/ +/// \see https://stackoverflow.com/questions/4127333/should-mutexes-be-mutable/4128689#4128689 +template +class Mutexed : public T +{ +public: + /// \brief Construct a new Mutexed + using T::T; + + /// \brief Assign a new value to the mutexed object, interpreted as a T. + /// \note NOT threadsafe! Must be used in a manner like this: + /// Mutexed mutexed("hello"); + /// { + /// std::scoped_lock lock{mutexed}; + /// mutexed = "hello"; + /// } + /// \see store, write + /// \return + template + Mutexed& operator=(Args&& ... args) + { + T::operator=(std::forward(args)...); + return *this; + } + + /// Intentionally slice object from type 'Mutexed' to 'T'. + [[nodiscard]] T slice() const + { return *this; } + + /// Intentionally slice object from type 'Mutexed' to 'T'. + [[nodiscard]] T& slice() + { return *this; } + + /// \brief Thread safe assignment of a new value to Mutexed + /// \tparam Args any types applicable to the creation of T + /// \param args any arguments applicable to the creation of T + template + void store(Args&& ... args) + { + std::scoped_lock lock{*this}; + T::operator=(std::forward(args)...); + } + + /// Thread safe slicing of object from type 'Mutexed' to 'T'. + /// Will create a copy, so this method is not applicable to non-copyable 'T's + /// \return + [[nodiscard]] T load() const + { + std::scoped_lock lock{*this}; + return slice(); + } + + /// \brief Provides unique access to the data by giving a `const T&` to the reader function. + /// A unique lock on the mutex is acquired prior to calling the supplied reader function, + /// so multiple simultaneous calls to read is not possible. + /// \tparam Invokable any invokable type that is invokable with `const T&` as argument + /// \param reader the invocable (a function or lambda, typically) + /// \return whatever the Invokable returns + template + auto read(const Invokable& reader) const + { + static_assert( + std::is_invocable_v, + "The provided `reader` is not invokable with `const T&` as argument" + ); + + std::scoped_lock lock{*this}; + return std::invoke(reader, slice()); + } + + /// \brief Provides unique access to the data by giving a `T&` to the writer function. + /// A unique lock on the mutex is acquired prior to calling the supplied writer function, + /// so simultaneous calls to read or write is not possible. + /// \tparam Invokable any invokable type that is invokable with `T&` as argument + /// \param writer the invocable (a function or lambda, typically) + /// \return whatever the Invokable returns + template + auto write(const Invokable& writer) + { + static_assert( + std::is_invocable_v, + "The provided `writer` is not invokable with `T&` as argument" + ); + + std::scoped_lock lock{*this}; + return std::invoke(writer, slice()); + } + + void lock() const ///< C++ named requirement: Mutex + { this->mu_.lock(); } + + void unlock() const ///< C++ named requirement: Mutex + { this->mu_.unlock(); }; + + bool try_lock() const noexcept ///< C++ named requirement: Mutex + { return this->mu_.try_lock(); } + +private: + mutable std::mutex mu_; +}; +} diff --git a/core/include/superflow/utils/pimpl_h.h b/core/include/superflow/utils/pimpl_h.h new file mode 100644 index 0000000..1ac3546 --- /dev/null +++ b/core/include/superflow/utils/pimpl_h.h @@ -0,0 +1,73 @@ +// Copyright (c) 2020 Forsvarets forskningsinstitutt (FFI). All rights reserved. +// https://herbsutter.com/gotw/_101/ + +#pragma once + +#include + +namespace flow +{ +/// \brief Helper class for the pimpl pattern. +/// The code is taken from Herb Sutter's +/// GotW#101, with slight modifications. +/// Read GotW#100for good practices and more about the Pimpl Idiom. +///
The \b pimpl_h.h file should be included from the header file of the class owning the pimpl object. +///
The \b pimpl_impl.h sould be included from the source file (cpp). Remember the explicit template instantiation! +/// +/// \b my_class.h +/// \code{.cpp} +/// #pragma once +/// #include "superflow/utils/pimpl_h.h" +/// class MyClass +/// { +/// // ... +/// private: +/// class impl; // forward declare +/// pimpl m_; // instead of std::unique_ptr +/// // ... +/// }; +/// \endcode +/// \b my_class.cpp +/// \code{.cpp} +/// #include "mylib/my_class.h" +/// #include "superflow/utils/pimpl_impl.h" +/// +/// // Easy to forget, but strictly required for the code to compile +/// template class flow::pimpl; +/// +/// // The impl. +/// class MyClass::impl +/// { +/// impl(impl ctor arguments); +/// void func(); +/// }; +/// +/// MyClass::MyClass(ctor args) +/// : m_{impl ctor arguments} // instead of std::make_unique +/// { +/// m_->func(); // Access the impl +/// } +/// \endcode +/// \tparam T The (typically private inner) class to be pimp'ed. +/// \see https://herbsutter.com/gotw/_100/ +/// \see https://herbsutter.com/gotw/_101/ +/// \see https://stackoverflow.com/questions/8595471/does-the-gotw-101-solution-actually-solve-anything +template +class pimpl +{ +private: + std::unique_ptr m; +public: + + template + pimpl(Args&& ...); + + ~pimpl(); + + pimpl& operator=(pimpl&&) noexcept; + + T* operator->() const; + + T& operator*() const; +}; +} \ No newline at end of file diff --git a/core/include/superflow/utils/pimpl_impl.h b/core/include/superflow/utils/pimpl_impl.h new file mode 100644 index 0000000..8cd34ee --- /dev/null +++ b/core/include/superflow/utils/pimpl_impl.h @@ -0,0 +1,29 @@ +// Copyright (c) 2020 Forsvarets forskningsinstitutt (FFI). All rights reserved. +// https://herbsutter.com/gotw/_101/ + +#pragma once + +#include + +namespace flow +{ +template +template +pimpl::pimpl(Args&& ...args) + : m{std::make_unique(std::forward(args)...)} +{} + +template +pimpl::~pimpl() = default; + +template +pimpl & pimpl::operator=(pimpl&&) noexcept = default; + +template +T* pimpl::operator->() const +{ return m.get(); } + +template +T& pimpl::operator*() const +{ return *m.get(); } +} \ No newline at end of file diff --git a/core/include/superflow/utils/proxel_timer.h b/core/include/superflow/utils/proxel_timer.h new file mode 100644 index 0000000..8ba09a1 --- /dev/null +++ b/core/include/superflow/utils/proxel_timer.h @@ -0,0 +1,80 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/proxel_status.h" + +#include +#include +#include +#include + +namespace flow +{ +/// \brief Utility class for measuring the workload of a Proxel. +/// +/// Typical usage: +/// \code{.cpp} +/// flow::ProxelTimer proxel_timer_; +/// // ... +/// for (const auto& data: *input_port_) +/// { +/// setState(State::Running); +/// proxel_timer_.start(); +/// // work ... +/// proxel_timer_.stop(); +/// setStatusInfo(proxel_timer_.getStatusInfo()); +/// } +/// \endcode +class ProxelTimer +{ +public: + /// \brief Start the timer + void start(); + + /// \brief Stop the timer + /// \return elapsed time since start (in seconds) + double stop(); + + /// \brief Read time since start without stopping the timer + /// \return elapsed time since start (in seconds) + [[nodiscard]] double peek() const; + + /// \brief Get time from start to stop, averaged over all starts and stops. + /// Measured in seconds. + /// \return average processing time (in seconds) + [[nodiscard]] double getAverageProcessingTime() const; + + /// \brief Get the ratio of processing vs idle state. + /// + /// A value of 1 is max busyness and means no idle time. + /// A value of 0 means no processing at all, only idle time. + /// + /// Computed as summed processing time divided by time since start was called for the first time.
+ /// \f[ Busyness = \frac{\sum_{i} {stop_i-start_i}}{stop_i-start_0} \f] + /// \return average busy ratio + [[nodiscard]] double getAverageBusyness() const; + + /// \brief Read how many times the timer has been stopped. + /// \return run count + [[nodiscard]] unsigned long long getRunCount() const; + + /// \brief Create a formatted string containing average processing time and average busyness. + /// \return The formatted string. + [[nodiscard]] std::string getStatusInfo() const; + +private: + using Clock = std::chrono::high_resolution_clock; + using Duration = std::chrono::duration; + + std::once_flag first_flag_; + Clock::time_point first_time_point_; + + std::atomic run_counter_{0}; + double summed_processing_time_{0}; + + std::atomic mean_processing_time_{0}; + std::atomic mean_busyness_time_{0}; + + Clock::time_point start_; +}; +} diff --git a/core/include/superflow/utils/shared_mutexed.h b/core/include/superflow/utils/shared_mutexed.h new file mode 100644 index 0000000..acfcb17 --- /dev/null +++ b/core/include/superflow/utils/shared_mutexed.h @@ -0,0 +1,133 @@ +// Copyright (c) 2019 Forsvarets forskningsinstitutt (FFI). All rights reserved. +#pragma once + +#include +#include +#include + +namespace flow +{ +/// \brief Exactly the same as Mutexed, except with a `shared_mutex` that +/// allows multiple concurrent reads. +/// It is better suited for scenarios were read operations are frequent and expensive. +/// \see Mutexed +/// \note Note that performing expensive operations while holding a lock is often a bad sign. +/// There may be better ways to solve the problem than using a SharedMutexed. +template +class SharedMutexed : public T +{ +public: + /// \brief Construct a new SharedMutexed + /// \tparam Args any types applicable to the constructor of T + /// \param args any arguments applicable to the constructor of T + template + explicit SharedMutexed(Args&& ... args) + : T(std::forward(args)...) + {} + + /// \brief Assign a new value to the mutexed object, interpreted as a T. + /// \note NOT threadsafe! Must be used in a manner like this: + /// SharedMutexed mutexed("hello"); + /// { + /// std::scoped_lock lock{mutexed}; + /// mutexed = "hello"; + /// } + /// \tparam Args any types applicable to the assignment of T + /// \param args any arguments applicable to the assignment of T + /// \see store, write + /// \return + template + SharedMutexed& operator=(Args&& ... args) + { + T& t = *this; + t = {std::forward(args)...}; + return *this; + } + + /// Intentionally slice object from type 'SharedMutexed' to 'T'. + [[nodiscard]] T slice() const + { return *this; } + + /// Intentionally slice object from type 'SharedMutexed' to 'T'. + [[nodiscard]] T& slice() + { return *this; } + + /// \brief Thread safe assignment of a new value to SharedMutexed + /// \tparam Args any types applicable to the creation of T + /// \param args any arguments applicable to the creation of T + template + void store(Args&& ... args) + { + T& t = *this; + std::scoped_lock lock{*this}; + t = {std::forward(args)...}; + } + + /// Thread safe slicing of object from type 'SharedMutexed' to 'T'. + /// Will create a copy, so this method is not applicable to non-copyable 'T's. + /// Concurrent load operations are possible + /// \return + [[nodiscard]] T load() const + { + std::shared_lock lock{*this}; + return slice(); + } + + /// \brief Provides shared access to the data by giving a `const T&` to the reader function. + /// A shared lock on the mutex is acquired prior to calling the supplied reader function, + /// so multiple simultaneous calls to read is possible. + /// \tparam Invokable any invokable type that is invokable with `const T&` as argument + /// \param reader the invocable (a function or lambda, typically) + /// \return whatever the Invokable returns + template + auto read(const Invokable& reader) const + { + static_assert( + std::is_invocable_v, + "The provided `reader` is not invokable with `const T&` as argument" + ); + + std::shared_lock lock{*this}; + return std::invoke(reader, slice()); + } + + /// \brief Provides unique access to the data by giving a `T&` to the writer function. + /// A unique lock on the mutex is acquired prior to calling the supplied writer function, + /// so simultaneous calls to read or write is not possible. + /// \tparam Invokable any invokable type that is invokable with `T&` as argument + /// \param writer the invocable (a function or lambda, typically) + /// \return whatever the Invokable returns + template + auto write(const Invokable& writer) + { + static_assert( + std::is_invocable_v, + "The provided `writer` is not invokable with `T&` as argument" + ); + + std::scoped_lock lock{*this}; + return std::invoke(writer, slice()); + } + + void lock() const ///< C++ named requirement: Mutex + { this->mu_.lock(); } + + void unlock() const ///< C++ named requirement: Mutex + { this->mu_.unlock(); }; + + bool try_lock() const noexcept ///< C++ named requirement: Mutex + { return this->mu_.try_lock(); } + + void lock_shared() const ///< C++ named requirement: SharedMutex + { this->mu_.lock_shared(); } + + void unlock_shared() const ///< C++ named requirement: SharedMutex + { this->mu_.unlock_shared(); }; + + bool try_lock_shared() const noexcept ///< C++ named requirement: SharedMutex + { return this->mu_.try_lock_shared(); } + +private: + mutable std::shared_mutex mu_; +}; +} diff --git a/core/include/superflow/utils/signal_waiter.h b/core/include/superflow/utils/signal_waiter.h new file mode 100644 index 0000000..3e40804 --- /dev/null +++ b/core/include/superflow/utils/signal_waiter.h @@ -0,0 +1,49 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include +#include +#include +#include +#include + +namespace flow +{ +/// \brief RAII-wrapper for thread-safe listening to one or more signals. +/// If you are using std::signal() or plain C signal() in your program, this class might not work as expected. +/// Use SignalWaiter::hasGottenSignal() at any time to check if one of the signals have been raised. +/// Use e.g. SignalWaiter::getFuture().wait() to wait until a signal has been received. +class SignalWaiter +{ +public: + /// \brief Creates a SignalWaiter that listens to all `signals`. + /// The signal handlers are guaranteed to have been installed once the CTOR returns. + explicit SignalWaiter( + const std::vector& signals = {SIGINT} + ); + + /// \brief Immediately stops listening to signals, as well as resolving all waiting futures + ~SignalWaiter(); + + /// \brief Returns whether any of the `signals` have been received + [[nodiscard]] bool hasGottenSignal() const; + + /// \brief Returns a future that is resolved as soon as one of the `signals` have been received, or the waiter + /// has been destroyed. + [[nodiscard]] std::shared_future getFuture() const; + +private: + std::mutex mutex_; + std::condition_variable cv_; + + bool is_waiting_; + bool got_signal_; + std::shared_ptr> handler_; + std::shared_future worker_; + + void handleSignal(); + + void await(const std::vector& signals); +}; +} + diff --git a/core/include/superflow/utils/sleeper.h b/core/include/superflow/utils/sleeper.h new file mode 100644 index 0000000..cf88b17 --- /dev/null +++ b/core/include/superflow/utils/sleeper.h @@ -0,0 +1,48 @@ +// Copyright 2022, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include + +namespace flow +{ +/// \brief Being the sister of Metronome and Throttle, Sleeper is the third way of rate control. +/// Typically intended to be used within a for-loop in order to stall execution. +/// Sleeper will sleep until another `period` has passed since the previous call to `sleepForRemainderOfPeriod`, thus a steady rate. +/// You can also adjust the sleep period with ´setNewSleepPeriod(Clock::duration period);` +/// +/// ```cpp +/// using namespace std::chrono_literals; +/// const Sleeper rate_limiter(10ms); +/// +/// for (const auto& data : *latched_port_) +/// { +/// do_work(); +/// rate_limiter.sleepForRemainderOfPeriod(); // Sleep for the rest of the period +/// +/// if(some condition) +/// { +/// rate_limiter.setNewSleepPeriod(xms); +/// } +/// } +/// ``` +/// +/// +/// If execution time of `do_work()` is less than 10ms, it will not be called again until 10ms has passed since +/// the previous call. If execution time is > 10ms, it will be called again immediately as sleepForRemainderOfPeriod time is already due. +/// +/// \see Metronome, Throttle +class Sleeper +{ +public: + using Clock = std::chrono::steady_clock; + + explicit Sleeper(Clock::duration period); + + void sleepForRemainderOfPeriod() const; + void setSleepPeriod(const Clock::duration period); + +private: + mutable Clock::time_point time_point_; + Clock::duration period_; +}; +} diff --git a/core/include/superflow/utils/terminated_exception.h b/core/include/superflow/utils/terminated_exception.h new file mode 100644 index 0000000..f40a4be --- /dev/null +++ b/core/include/superflow/utils/terminated_exception.h @@ -0,0 +1,22 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include + +namespace flow +{ +struct TerminatedException : public std::runtime_error +{ + TerminatedException() + : std::runtime_error("LockQueue is terminated") + {} + + explicit TerminatedException(const std::string& what_arg) + : std::runtime_error(what_arg) + {} + + explicit TerminatedException(const char* what_arg) + : std::runtime_error(what_arg) + {} +}; +} diff --git a/core/include/superflow/utils/throttle.h b/core/include/superflow/utils/throttle.h new file mode 100644 index 0000000..4dc8a13 --- /dev/null +++ b/core/include/superflow/utils/throttle.h @@ -0,0 +1,116 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include +#include +#include +#include +#include + +namespace flow +{ +/// \brief This is a utility for limiting the rate of which a function is called. +/// +/// The target function will be called periodically at a requested rate, using an +/// internal timer. +/// If new data is provided repeatedly while the throttler is stalling, only the latest data +/// will be buffered. When the timer expires, that last data will be sent. +/// If the timer expires without any new data being available, nothing happens. +/// If new data is provided after the timer expires, it is sent immediately +/// and the timer starts again. +/// \tparam T the type of data sent. +/// +/// \see Metronome, Sleeper +template +class Throttle +{ + +public: + /// Signature of the target function + using Callback = std::function; + + using Duration = std::chrono::steady_clock::duration; + + /// \brief Create a new Throttle. + /// If the callback throws an exception, it will be caught and propagated + /// to the next caller of push. + /// \param cb the function to be called if data is available when the timer expires. + /// \param delay the rate at which the given function is called + Throttle(Callback cb, const Duration& delay); + + ~Throttle(); + + /// \brief Push new data to the Throttle. + /// \param data to be sent to the target function. + /// \throws any exception that may occur in the provided callback function. + /// If an exception has been raised in the previous run of the worker thread, this function + /// will propagate the exception to the current caller. + template + void push(U&& data); + +private: + bool stopped_ = false; + bool new_data_ = false; + std::mutex mu_; + std::condition_variable cv_; + T data_; + Callback callback_; + Duration delay_; + std::future runner_; + + void run(); +}; + +// --- Implementation --- // + +template +Throttle::Throttle(Callback cb, const Duration& delay) + : callback_{std::move(cb)} + , delay_{delay} + , runner_{std::async(std::launch::async, [this]{ run(); })} +{} + +template +Throttle::~Throttle() +{ + stopped_ = true; + cv_.notify_all(); +} + +template +void Throttle::run() +{ + while (!stopped_) + { + std::unique_lock lock(mu_); + cv_.wait(lock, [this]{ return (stopped_ || new_data_); }); + + if (stopped_) + { break; } + + try + { callback_(std::move(data_)); } + catch (...) + { + stopped_ = true; + throw; + } + new_data_ = false; + cv_.wait_for(lock, delay_, [this]{return stopped_;}); + } +} + +template +template +void Throttle::push(U&& data) +{ + if (stopped_) + { runner_.get(); } + { + std::scoped_lock lock(mu_); + data_ = std::forward(data); + new_data_ = true; + } + cv_.notify_one(); +} +} diff --git a/core/include/superflow/utils/wait_for_signal.h b/core/include/superflow/utils/wait_for_signal.h new file mode 100644 index 0000000..ee73d79 --- /dev/null +++ b/core/include/superflow/utils/wait_for_signal.h @@ -0,0 +1,13 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include +#include + +namespace flow +{ +/// \brief Make the current thread wait for the specified signal(s). +/// This is thread-safe and can be used on multiple threads simultaneously. +/// \param signals The signals to wait for (default is SIGINT). +void waitForSignal(const std::vector& signals = {SIGINT}); +} diff --git a/core/include/superflow/value.h b/core/include/superflow/value.h new file mode 100644 index 0000000..212a05a --- /dev/null +++ b/core/include/superflow/value.h @@ -0,0 +1,45 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include + +namespace flow +{ +/// \brief Extract a value from a PropertyList. +/// +/// A PropertyList is a templated type that must support extraction of values by referring to their names. +/// It is required that a valid PropertyList defines the following functions: +///

    +///
  • `bool hasKey(const std::string& key)`, tells whether the given key exists or not.
  • +///
  • `T convertValue(const std::string& key)`, retrieves a value from the list.
  • +///
+/// \tparam T The value type +/// \tparam PropertyList The concrete type of PropertyList +/// \param properties the property list +/// \param key The identifier of the value +/// \return The value. +/// \note May throw if the key does not exist (depends on the implementation of PropertyList) +template +T value(const PropertyList& properties, const std::string& key) +{ + return properties.template convertValue(key); +} + +/// Same as above, but will return default_value if the 'key' does not exist. +template +T value(const PropertyList& properties, const std::string& key, const T& default_value) +{ + return properties.hasKey(key) + ? properties.template convertValue(key) + : default_value; +} + +/// Same as above, but will return a default value constructed by 'default_args' if 'key' does not exist. +template +T value(const PropertyList& properties, const std::string& key, Args... default_args) +{ + return properties.hasKey(key) + ? properties.template convertValue(key) + : T{default_args...}; +} +} diff --git a/core/src/graph.cpp b/core/src/graph.cpp new file mode 100644 index 0000000..fe34c46 --- /dev/null +++ b/core/src/graph.cpp @@ -0,0 +1,205 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/graph.h" +#include "superflow/utils/metronome.h" + +#include +#include + +namespace flow +{ +Graph::Graph(std::map proxels) + : proxels_{std::move(proxels)} +{} + +Graph::~Graph() +{ + stop(); +} + +void Graph::add(const std::string& proxel_id, Proxel::Ptr&& proxel) +{ + if (proxels_.count(proxel_id) > 0) + { throw std::invalid_argument(std::string("Proxel '" + proxel_id + "' does already exist")); } + proxels_.emplace(proxel_id, std::move(proxel)); +} + +void Graph::start( + const bool handle_exceptions, + const CrashLogger& crash_logger +) +{ + if (isRunning()) + { throw std::runtime_error("Cannot start Graph when threads are running"); } + + for (const auto& kv : proxels_) + { + const auto& proxel_name = kv.first; + const auto& proxel = kv.second; + + if (handle_exceptions) + { + proxel_threads_.emplace( + proxel_name, + [this, proxel_name, proxel, crash_logger]() + { + try + { + proxel->start(); + } + catch (const std::exception& e) + { + crashes_[proxel_name] = e.what(); + + if (crash_logger) + { + crash_logger(proxel_name, e.what()); + } + } + catch (...) + { + crashes_[proxel_name] = "unknown exception"; + + if (crash_logger) + { + crash_logger(proxel_name, "Unknown exception"); + } + } + } + ); + } + else + { + proxel_threads_.emplace( + proxel_name, + [&proxel]() + { + proxel->start(); + } + ); + } + } +} + +void Graph::stop() +{ + if (!isRunning()) + { return; } + + for (auto& proxel : proxels_) + { + proxel.second->stop(); + } + + for (const auto& kv : proxels_) + { + const auto& proxel_name = kv.first; + const auto thread_it = proxel_threads_.find(proxel_name); + + if (thread_it == proxel_threads_.end()) + { + continue; + } + + auto& proc_thread = thread_it->second; + + Metronome repeater{ + [&proxel_name](const auto& duration) + { + const auto seconds = std::chrono::duration_cast(duration); + + std::cerr << "Still waiting for " << proxel_name << " to finish after " << seconds.count() << "s of waiting" << std::endl; + }, + std::chrono::seconds{2} + }; + + if (proc_thread.joinable()) + { proc_thread.join(); } + + repeater.stop(); + } + + proxel_threads_.clear(); +} + +void Graph::connect( + const std::string& proxel1, + const std::string& proxel1_port, + const std::string& proxel2, + const std::string& proxel2_port) const +{ + if (proxel1 == proxel2) + { + throw std::invalid_argument{"Loop detected trying to connect \"" + proxel1 + "\" to itself."}; + } + + try + { + auto& port1 = getProxel(proxel1)->getPort(proxel1_port); + auto& port2 = getProxel(proxel2)->getPort(proxel2_port); + + if (port1 == nullptr) + { throw std::invalid_argument(proxel1 + "." + proxel1_port + " is a nullptr."); } + + if (port2 == nullptr) + { throw std::invalid_argument(proxel2 + "." + proxel2_port + " is a nullptr."); } + + port1->connect(port2); + } + catch (const std::invalid_argument& e) + { + std::ostringstream ss; + ss << "Connect " + << proxel1 << "." << proxel1_port + << " -> " + << proxel2 << "." << proxel2_port + << " failed:\n\t" + << e.what(); + + throw std::invalid_argument(ss.str()); + } +} + +std::map Graph::getProxelStatuses() const +{ + std::map statuses; + + for (const auto& proxel : proxels_) + { + const std::string& name = proxel.first; + + auto it = crashes_.find(name); + + if (it != crashes_.end()) + { + const std::string& error_message = it->second; + + statuses[name] = ProxelStatus{ + ProxelStatus::State::Crashed, + error_message, + {} + }; + } else + { + statuses[name] = proxel.second->getStatus(); + } + } + + return statuses; +} + +bool Graph::isRunning() const +{ + return !proxel_threads_.empty(); +} + +const Graph::CrashLogger Graph::quietCrashLogger = nullptr; + +void Graph::defaultCrashLogger(const std::string& proxel_name, const std::string& what) +{ + std::cerr + << "Proxel '" << proxel_name + << "' crashed with exception:\n \"" + << what << "\"" + << std::endl; +} +} diff --git a/core/src/graphviz.cpp b/core/src/graphviz.cpp new file mode 100644 index 0000000..8277882 --- /dev/null +++ b/core/src/graphviz.cpp @@ -0,0 +1,116 @@ +#include "superflow/utils/graphviz.h" + +#include +#include +#include +#include + +namespace flow +{ +namespace +{ +std::string portFormatting(const std::string& port_name) +{ + return "<" + port_name + "> " + port_name; +} + +std::string strToUpper(std::string str) +{ + std::transform( + str.begin(), str.end(), str.begin(), + [](unsigned char c) + { return std::toupper(c); } + ); + return str; +} + +std::string join(const std::set& data) +{ + if (data.empty()) + { return {}; } + + return std::accumulate( + std::next(data.begin()), data.end(), + portFormatting(*(data.begin())), + [](std::string a, const std::string& b) + { return std::move(a) + "| " + portFormatting(b); } + ); +} +} + +GraphViz::GraphViz( + const std::vector& connections +) +{ + for (const auto& connection : connections) + { + insert(connection); + } +} + +void GraphViz::insert(const ConnectionSpec& connection) +{ + if (connection.lhs_name.empty()) + { throw std::invalid_argument("ConnectionSpec must at least have lhs_name."); } + + if (connection.lhs_port.empty() || connection.rhs_name.empty() || connection.rhs_port.empty()) + { + node_list[connection.lhs_name]; + return; + } + + node_list[connection.lhs_name].adjacency_list.insert(connection); + node_list[connection.lhs_name].lhs_ports.insert(connection.lhs_port); + node_list[connection.rhs_name].rhs_ports.insert(connection.rhs_port); +} + +std::string GraphViz::getNodeDefinitions() const +{ + std::ostringstream os; + + for (const auto& [node_name, node]: node_list) + { + std::string out = join(node.lhs_ports); + std::string in = join(node.rhs_ports); + os << " " << node_name + << " [label=\"{" + << "{ " << in << "} | " + << strToUpper(node_name) << " | " + << "{ " << out << "} " + << "}\"]\n"; + } + + return os.str(); +} + +std::string GraphViz::getNodeConnections() const +{ + std::ostringstream os; + + for (const auto& [node_name, node]: node_list) + { + for (const auto& conn: node.adjacency_list) + { + if (conn.lhs_port.empty() || conn.rhs_name.empty() || conn.rhs_port.empty()) + { continue; } + + os << " " << conn.lhs_name << ":" << conn.lhs_port << " -> " << conn.rhs_name << ":" << conn.rhs_port << "\n"; + } + } + return os.str(); +} + +std::string GraphViz::employ() const +{ + std::ostringstream gv; + gv << "digraph superflow {\n"; + gv << " rankdir=\"LR\";\n"; + gv << " node [shape=Mrecord];\n"; + gv << getNodeDefinitions(); + gv << "\n"; + gv << getNodeConnections(); + gv << "}\n"; + + return gv.str(); +} +} diff --git a/core/src/metronome.cpp b/core/src/metronome.cpp new file mode 100644 index 0000000..23a2ec4 --- /dev/null +++ b/core/src/metronome.cpp @@ -0,0 +1,82 @@ +#include "superflow/utils/metronome.h" + +namespace flow +{ +using Clock = std::chrono::steady_clock; + +Metronome::Metronome( + const Functional& func, + const Duration period +) + : has_stopped_{false} +{ + const auto first_time_point = Clock::now(); + + worker_ = std::async( + std::launch::async, + [this, func, period, first_time_point]() + { + auto time_point = first_time_point; + + while (!has_stopped_) + { + time_point += period; + + { + std::unique_lock lock{mutex_}; + const bool has_stopped = cv_.wait_until(lock, time_point, + [this]{ return has_stopped_; } + ); + if (has_stopped) + { break; } + } + + const auto duration_since_begin = Clock::now() - first_time_point; + + func(duration_since_begin); + } + } + ); +} + +Metronome::~Metronome() +{ + stop(); +} + +void Metronome::get() +{ + if (worker_.valid()) + { + worker_.get(); + } + else + { + throw std::runtime_error{"Cannot call get() on Metronome with invalid state"}; + } +} + +void Metronome::check() +{ + const auto state = worker_.wait_for(std::chrono::seconds{0}); + + if (state != std::future_status::timeout) + { + get(); + } +} + +void Metronome::stop() noexcept +{ + { + std::scoped_lock lock{mutex_}; + + if (has_stopped_) + { return; } + + has_stopped_ = true; + } + + cv_.notify_one(); +} +} diff --git a/core/src/port_manager.cpp b/core/src/port_manager.cpp new file mode 100644 index 0000000..00c6c13 --- /dev/null +++ b/core/src/port_manager.cpp @@ -0,0 +1,58 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/port_manager.h" + +#include + +namespace flow +{ +PortManager::PortManager(const PortMap& ports) + : ports_{ports} +{} + +PortManager::PortManager(PortMap&& ports) + : ports_{std::move(ports)} +{} + +PortManager::~PortManager() +{ + for (auto& kv : ports_) + { + Port::Ptr& port = kv.second; + + if (port == nullptr) + { + continue; + } + + port->disconnect(); + } +} + +const Port::Ptr& PortManager::get(const std::string& name) const +{ + try + { return ports_.at(name); } + catch (...) + { throw std::invalid_argument(std::string("port '" + name + "' does not exist")); } +} + +const PortManager::PortMap& PortManager::getPorts() const +{ + return ports_; +} + +std::map PortManager::getStatus() const +{ + std::map statuses; + + for (const auto& [port_name, port] : ports_) + { + if (port == nullptr) + { continue; } + + statuses[port_name] = port->getStatus(); + } + + return statuses; +} +} diff --git a/core/src/proxel.cpp b/core/src/proxel.cpp new file mode 100644 index 0000000..deefda4 --- /dev/null +++ b/core/src/proxel.cpp @@ -0,0 +1,49 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/proxel.h" + +namespace flow +{ +const Port::Ptr& Proxel::getPort(const std::string& name) const +{ + return port_manager_.get(name); +} + +const PortManager::PortMap& Proxel::getPorts() const +{ + return port_manager_.getPorts(); +} + +ProxelStatus Proxel::getStatus() const +{ + return { + getState(), + getStatusInfo(), + port_manager_.getStatus() + }; +} + +void Proxel::setState(const State state) const +{ + state_ = state; +} + +void Proxel::setStatusInfo(const std::string& status_info) const +{ + status_info_.store(status_info); +} + +Proxel::State Proxel::getState() const +{ + return state_; +} + +std::string Proxel::getStatusInfo() const +{ + return status_info_.load(); +} + +void Proxel::registerPorts(PortManager::PortMap&& ports) +{ + port_manager_ = PortManager{std::move(ports)}; +} +} diff --git a/core/src/proxel_timer.cpp b/core/src/proxel_timer.cpp new file mode 100644 index 0000000..6f0553c --- /dev/null +++ b/core/src/proxel_timer.cpp @@ -0,0 +1,67 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/utils/proxel_timer.h" + +#include +#include + +namespace flow +{ +void ProxelTimer::start() +{ + start_ = Clock::now(); + std::call_once(first_flag_, [this]() + { first_time_point_ = start_; }); +} + +double ProxelTimer::stop() +{ + if (Duration(first_time_point_.time_since_epoch()).count() == 0.) + { throw std::runtime_error("ProxelTimer::stop() has been called before ProxelTimer::start()"); } + + const auto now = Clock::now(); + + const double processing_time = Duration{now - start_}.count(); + summed_processing_time_ += processing_time; + + ++run_counter_; + mean_processing_time_ = summed_processing_time_ / static_cast(run_counter_); + + const auto uptime = Duration{now - first_time_point_}.count(); + mean_busyness_time_ = summed_processing_time_ / uptime; + + return processing_time; +} + +double ProxelTimer::peek() const +{ + const auto now = Clock::now(); + const double processing_time = Duration{now - start_}.count(); + return processing_time; +} + +double ProxelTimer::getAverageProcessingTime() const +{ + return mean_processing_time_; +} + +double ProxelTimer::getAverageBusyness() const +{ + return mean_busyness_time_; +} + +unsigned long long ProxelTimer::getRunCount() const +{ + return run_counter_; +} + +std::string ProxelTimer::getStatusInfo() const +{ + std::ostringstream ss; + ss << std::fixed << std::setprecision(3) + << "time: " << getAverageProcessingTime() << "s" + << "\n" + << "busy: " << getAverageBusyness(); + + return ss.str(); +} +} diff --git a/core/src/signal_waiter.cpp b/core/src/signal_waiter.cpp new file mode 100644 index 0000000..a9b3831 --- /dev/null +++ b/core/src/signal_waiter.cpp @@ -0,0 +1,147 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/utils/signal_waiter.h" + +#include +#include + +namespace flow +{ +namespace +{ +using Handler = std::shared_ptr>; + +std::mutex handler_mutex; +std::map> handlers; + +void register_handler(int signal, const Handler& handler); + +void unregister_handler(int signal, const Handler& handler); + +void handle_signal(int signal); +} + +SignalWaiter::SignalWaiter( + const std::vector& signals +) + : is_waiting_{true} + , got_signal_{false} + , handler_{std::make_shared>([this](){handleSignal();})} +{ + for (const auto signal : signals) + { + register_handler(signal, handler_); + } + + worker_ = std::async( + std::launch::async, + [this, signals]() + { + await(signals); + } + ); +} + +SignalWaiter::~SignalWaiter() +{ + is_waiting_ = false; + cv_.notify_one(); + worker_.wait(); +} + +bool SignalWaiter::hasGottenSignal() const +{ + return got_signal_; +} + +std::shared_future SignalWaiter::getFuture() const +{ + return worker_; +} + +void SignalWaiter::handleSignal() +{ + got_signal_ = true; + cv_.notify_one(); +} + +void SignalWaiter::await(const std::vector& signals) +{ + std::unique_lock lock{mutex_}; + cv_.wait( + lock, + [this]() + { + return !is_waiting_ || got_signal_; + } + ); + + for (const auto signal : signals) + { + unregister_handler(signal, handler_); + } +} + +namespace +{ +void register_handler( + const int signal, + const Handler& handler +) +{ + std::lock_guard lock{handler_mutex}; + + auto it = handlers.find(signal); + + if (it == handlers.end()) + { + it = handlers.insert({signal, {}}).first; + std::signal(signal, handle_signal); + } + + it->second.insert(handler); +} + +void unregister_handler( + const int signal, + const Handler& handler +) +{ + std::lock_guard lock{handler_mutex}; + + const auto it = handlers.find(signal); + + if (it == handlers.end()) + { + return; + } + + it->second.erase(handler); + + if (it->second.empty()) + { + // no handlers are registered with this signal + // so set it back to default + + std::signal(signal, SIG_DFL); + handlers.erase(it); + } +} + +void handle_signal(const int signal) +{ + std::lock_guard lock{handler_mutex}; + + const auto it = handlers.find(signal); + + if (it == handlers.end()) + { + return; + } + + for (const auto& handler : it->second) + { + (*handler)(); + } +} +} +} diff --git a/core/src/sleeper.cpp b/core/src/sleeper.cpp new file mode 100644 index 0000000..3d56b56 --- /dev/null +++ b/core/src/sleeper.cpp @@ -0,0 +1,21 @@ +// Copyright (c) 2022, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/utils/sleeper.h" +#include + +namespace flow +{ + +Sleeper::Sleeper(Clock::duration period) + : time_point_{Clock::now()} + , period_(period) +{} + +void Sleeper::sleepForRemainderOfPeriod() const { + time_point_ += period_; + std::this_thread::sleep_until(time_point_); +} + +void Sleeper::setSleepPeriod(const Clock::duration period) { + period_ = period; +} +} diff --git a/core/src/wait_for_signal.cpp b/core/src/wait_for_signal.cpp new file mode 100644 index 0000000..c3717ad --- /dev/null +++ b/core/src/wait_for_signal.cpp @@ -0,0 +1,13 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/utils/wait_for_signal.h" + +#include "superflow/utils/signal_waiter.h" + +namespace flow +{ +void waitForSignal(const std::vector& signals) +{ + const SignalWaiter waiter{signals}; + waiter.getFuture().wait(); +} +} diff --git a/core/test/CMakeLists.txt b/core/test/CMakeLists.txt new file mode 100644 index 0000000..e052236 --- /dev/null +++ b/core/test/CMakeLists.txt @@ -0,0 +1,52 @@ +set(PARENT_PROJECT ${PROJECT_NAME}) +set(target_test_name "${CMAKE_PROJECT_NAME}-${PARENT_PROJECT}-test") +project(${target_test_name} CXX) +message(STATUS "* Adding test executable '${target_test_name}'") + +add_executable(${target_test_name} + "connectable_port.h" + "connectable_proxel.h" + "crashing_proxel.h" + "multi_connectable_port.h" + "pimpl_test.cpp" + "templated_testproxel.h" + "test_block_lock_queue.cpp" + "test_buffered_consumer_port.cpp" + "test_callback_consumer_port.cpp" + "test_connection_manager.cpp" + "test_graph_factory.cpp" + "test_graph.cpp" + "test_interface_port.cpp" + "test_lock_queue.cpp" + "test_multi_lock_queue.cpp" + "test_pimpl.cpp" + "test_requester_responder_port.cpp" + "test_metronome.cpp" + "test_multi_consumer_port.cpp" + "test_multi_queue_getter.cpp" + "test_multi_requester_port.cpp" + "test_mutexed.cpp" + "test_port_manager.cpp" + "test_proxel_status.cpp" + "test_producer_consumer_port.cpp" + "test_proxel_timer.cpp" + "test_shared_mutexed.cpp" + "test_signal_waiter.cpp" + "test_sleeper.cpp" + "test_throttle.cpp" + "threaded_proxel.h" +) + +target_link_libraries( + ${target_test_name} + PRIVATE GTest::gtest GTest::gtest_main + PRIVATE ${CMAKE_PROJECT_NAME}::core +) + +set_target_properties(${target_test_name} PROPERTIES + CXX_STANDARD_REQUIRED ON + CXX_STANDARD 17 +) + +include(GoogleTest) +gtest_discover_tests(${target_test_name}) diff --git a/core/test/connectable_port.h b/core/test/connectable_port.h new file mode 100644 index 0000000..e166c6b --- /dev/null +++ b/core/test/connectable_port.h @@ -0,0 +1,72 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/port.h" + +#include + +namespace flow +{ +template +class ConnectablePort : + public Port, + public std::enable_shared_from_this> +{ +public: + using Ptr = std::shared_ptr; + + std::shared_ptr connection_; + size_t num_connections = PortStatus::undefined; + size_t num_transactions = PortStatus::undefined; + bool did_get_disconnect = false; + + void connect(const Port::Ptr& ptr) override + { + auto connection = std::dynamic_pointer_cast(ptr); + + if (connection == nullptr) + { + throw std::invalid_argument("Mismatch between port types"); + } + + connection_ = std::move(connection); + connection_->connection_ = this->shared_from_this(); + } + + void disconnect() noexcept override + { + did_get_disconnect = true; + + if (connection_ == nullptr) + { + return; + } + + connection_->connection_ = nullptr; + connection_ = nullptr; + } + + void disconnect(const Port::Ptr& ptr) noexcept override + { + if (connection_ != ptr) + { + return; + } + + disconnect(); + } + + bool isConnected() const override + { + return connection_ != nullptr; + } + + PortStatus getStatus() const override + { + return { + num_connections, + num_transactions + }; + } +}; +} diff --git a/core/test/connectable_proxel.h b/core/test/connectable_proxel.h new file mode 100644 index 0000000..d588ee8 --- /dev/null +++ b/core/test/connectable_proxel.h @@ -0,0 +1,35 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/proxel.h" + +namespace flow +{ +class ConnectableProxel : public Proxel +{ +public: + ConnectableProxel( + const Port::Ptr& in_port, + const Port::Ptr& out_port + ) + : in_port_{in_port} + , out_port_{out_port} + { + registerPorts( + { + {"inport", in_port_}, + {"outport", out_port_} + } + ); + } + + void start() override + {}; + + void stop() noexcept override + {}; + + Port::Ptr in_port_; + Port::Ptr out_port_; +}; +} diff --git a/core/test/crashing_proxel.h b/core/test/crashing_proxel.h new file mode 100644 index 0000000..acc39d1 --- /dev/null +++ b/core/test/crashing_proxel.h @@ -0,0 +1,19 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/proxel.h" + +namespace flow +{ +class CrashingProxel : public Proxel +{ +public: + void start() override + { + throw std::runtime_error("This proxel has crashed"); + } + + void stop() noexcept override + {} +}; +} diff --git a/core/test/multi_connectable_port.h b/core/test/multi_connectable_port.h new file mode 100644 index 0000000..804d322 --- /dev/null +++ b/core/test/multi_connectable_port.h @@ -0,0 +1,85 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/port.h" + +#include + +namespace flow +{ +template +class MultiConnectablePort : + public Port, + public std::enable_shared_from_this> +{ +public: + using Ptr = std::shared_ptr; + std::unordered_set> connections_; + bool did_get_disconnect = false; + + void connect(const Port::Ptr& ptr) override + { + auto connection = std::dynamic_pointer_cast(ptr); + + if (connection == nullptr) + { + throw std::invalid_argument("Mismatch between port types"); + } + + connections_.insert(connection); + connection->connections_.insert(this->shared_from_this()); + } + + void disconnect() noexcept override + { + did_get_disconnect = true; + + for (const auto& connection : connections_) + { + connection->connections_.erase(this->shared_from_this()); + } + + connections_.clear(); + } + + void disconnect(const Port::Ptr& ptr) noexcept override + { + did_get_disconnect = true; + + auto connection = std::dynamic_pointer_cast(ptr); + + if (connection == nullptr) + { + return; + } + + auto it = connections_.find(connection); + + if (it == connections_.end()) + { + return; + } + + (*it)->connections_.erase(this->shared_from_this()); + connections_.erase(it); + } + + bool isConnected() const override + { + return !connections_.empty(); + } + + size_t getNumConnections() const + { + return connections_.size(); + } + + PortStatus getStatus() const override + { + return { + getNumConnections(), + 0 + }; + } +}; +} diff --git a/core/test/pimpl_test.cpp b/core/test/pimpl_test.cpp new file mode 100644 index 0000000..b7dfa16 --- /dev/null +++ b/core/test/pimpl_test.cpp @@ -0,0 +1,16 @@ +// Copyright (c) 2020 Forsvarets forskningsinstitutt (FFI). All rights reserved. + +#include "pimpl_test.h" +#include "superflow/utils/pimpl_impl.h" + +template class flow::pimpl; + +namespace flow::test +{ +class A::impl +{}; + +A::A() + : m_{} +{} +} \ No newline at end of file diff --git a/core/test/pimpl_test.h b/core/test/pimpl_test.h new file mode 100644 index 0000000..7ab7843 --- /dev/null +++ b/core/test/pimpl_test.h @@ -0,0 +1,18 @@ +// Copyright (c) 2020 Forsvarets forskningsinstitutt (FFI). All rights reserved. +#pragma once + +#include "superflow/utils/pimpl_h.h" + +namespace flow::test +{ +class A +{ +public: + A(); + +private: + class impl; + + flow::pimpl m_; +}; +} \ No newline at end of file diff --git a/core/test/templated_testproxel.h b/core/test/templated_testproxel.h new file mode 100644 index 0000000..c390f7f --- /dev/null +++ b/core/test/templated_testproxel.h @@ -0,0 +1,65 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/buffered_consumer_port.h" +#include "superflow/policy.h" +#include "superflow/port_manager.h" +#include "superflow/proxel.h" +#include "superflow/producer_port.h" +#include "superflow/value.h" + +#include + +namespace flow +{ +template +class TemplatedProxel : public Proxel +{ +public: + explicit TemplatedProxel(T init_value = {}) + : value_{std::move(init_value)} + , out_port_{std::make_shared()} + , in_port_{std::make_shared()} + { + registerPorts( + { + {"outport", out_port_}, + {"inport", in_port_} + }); + } + + void start() override + { out_port_->send(value_); } + + void stop() noexcept override + {} + + T getValue() + { + (*in_port_) >> value_; + return value_; + } + + T getStoredValue() const + { return value_; } + + template + static flow::Proxel::Ptr create( + const PropertyList& properties + ) + { + return std::make_shared>( + value(properties, "init_value") + ); + } + +private: + using InPort = BufferedConsumerPort; + using OutPort = ProducerPort; + + T value_; + + typename OutPort::Ptr out_port_; + typename InPort::Ptr in_port_; +}; +} diff --git a/core/test/test_block_lock_queue.cpp b/core/test/test_block_lock_queue.cpp new file mode 100644 index 0000000..9fd34a9 --- /dev/null +++ b/core/test/test_block_lock_queue.cpp @@ -0,0 +1,414 @@ +#include "superflow/utils/lock_queue.h" + +#include "gtest/gtest.h" + +#include +#include + +using namespace flow; +using namespace std::chrono_literals; + +TEST(BlockLockQueue, QueueSizeZeroThrows) +{ + ASSERT_THROW((LockQueue(0)), std::invalid_argument); +} + +TEST(BlockLockQueue, PushLvalue) +{ + LockQueue impl(10); + int val = 42; + ASSERT_NO_FATAL_FAILURE(impl.push(val)); +} + +TEST(BlockLockQueue, PushRvalue) +{ + LockQueue impl(10); + ASSERT_NO_FATAL_FAILURE(impl.push(42)); +} + +TEST(BlockLockQueue, Queue_size_from_const_reference_works) +{ + LockQueue impl(10); + const auto& cref = impl; + + ASSERT_EQ(0u, cref.getQueueSize()); +} + +TEST(BlockLockQueue, PushIncreasesQueueSize) +{ + LockQueue impl(10); + ASSERT_EQ(0u, impl.getQueueSize()); + impl.push(42); + ASSERT_EQ(1u, impl.getQueueSize()); +} + +TEST(BlockLockQueue, InitializerListInitializedQueue) +{ + LockQueue impl(10, {42, 2, 3}); + EXPECT_EQ(3u, impl.getQueueSize()); + EXPECT_EQ(42, impl.pop()); +} + +TEST(BlockLockQueue, TooLongInitializerListThrowsException) +{ + EXPECT_THROW((LockQueue(2, {42, 2, 3})), std::range_error); +} + +TEST(BlockLockQueue, PopReturnsInsertedValue) +{ + LockQueue impl(10); + const int val = 42; + + // Test T pop() + impl.push(val); + impl.push(val + 1); + EXPECT_EQ(val, impl.pop()); + EXPECT_EQ(val + 1, impl.pop()); + + // Test void pop(T&) + EXPECT_EQ(0u, impl.getQueueSize()); + impl.push(val); + impl.push(val + 1); + int res = 0; + EXPECT_NO_FATAL_FAILURE(impl.pop(res)); + EXPECT_EQ(val, res); + EXPECT_NO_FATAL_FAILURE(impl.pop(res)); + EXPECT_EQ(val + 1, res); +} + +TEST(BlockLockQueue, PopDecreasesQueueSize) +{ + LockQueue impl(10); + ASSERT_EQ(0u, impl.getQueueSize()); + impl.push(42); + ASSERT_EQ(1u, impl.getQueueSize()); + impl.pop(); + ASSERT_EQ(0u, impl.getQueueSize()); +} + +TEST(BlockLockQueue, PopHangsUntilPushAndDoesNotThrow) +{ + LockQueue impl(10); + int popped_value = 0; + auto push_fn = [&impl]() + { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + impl.push(42); + }; + std::thread pusher(push_fn); + ASSERT_TRUE(pusher.joinable()); + EXPECT_EQ(0, popped_value); + + EXPECT_NO_THROW(popped_value = impl.pop()); + + pusher.join(); + EXPECT_EQ(42, popped_value); +} + +TEST(BlockLockQueue, PopHangsUntilTerminateAndThenThrows) +{ + LockQueue impl(10); + + int popped_value = 42; + + std::promise terminated; + + auto term_fn = [&impl, &terminated]() + { + EXPECT_NO_THROW(impl.terminate()); + terminated.set_value(); + }; + + std::thread terminator(term_fn); + { + const auto future_status = terminated.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + } + ASSERT_TRUE(terminator.joinable()); + + EXPECT_THROW(popped_value = impl.pop(), TerminatedException); + terminator.join(); + EXPECT_EQ(42, popped_value); +} + +TEST(BlockLockQueue, PushHangsUntilTerminateAndThenThrows) +{ + LockQueue impl(2); + + impl.push(1); + impl.push(2); + std::promise pushed; + + auto blocked_push = std::async(std::launch::async, [&impl, &pushed] + { + EXPECT_NO_THROW(impl.push(3)); + pushed.set_value(); + impl.push(4); + }); + + auto future = pushed.get_future(); + EXPECT_EQ(future.wait_for(10ms), std::future_status::timeout); + + impl.pop(); + + EXPECT_EQ(future.wait_for(1s), std::future_status::ready); + EXPECT_NO_THROW(impl.terminate()); + EXPECT_EQ(blocked_push.wait_for(1s), std::future_status::ready); + EXPECT_THROW(blocked_push.get(), TerminatedException); +} + +TEST(BlockLockQueue, TerminateTwiceDoesNotThrow) +{ + LockQueue impl(10); + EXPECT_NO_THROW(impl.terminate()); + EXPECT_NO_THROW(impl.terminate()); +} + +TEST(BlockLockQueue, Queue_correctly_responds_if_it_is_terminated_or_not) +{ + LockQueue impl(10); + std::promise terminate; + std::promise terminated; + + auto term_fn = [&impl, &terminate, &terminated]() + { + terminate.get_future().wait(); + EXPECT_NO_THROW(impl.terminate()); + terminated.set_value(); + }; + + std::thread terminator(term_fn); + ASSERT_TRUE(terminator.joinable()); + + EXPECT_FALSE(impl.isTerminated()); + terminate.set_value(); + + terminated.get_future().wait(); + EXPECT_TRUE(impl.isTerminated()); + + terminator.join(); +} + +TEST(BlockLockQueue, PushMoreThanCapacityBlocksPush) +{ + constexpr uint32_t queue_size = 10; + LockQueue impl(queue_size); + ASSERT_EQ(0u, impl.getQueueSize()); + for (uint32_t i = 0; i < queue_size; ++i) + { + impl.push(i); + } + ASSERT_EQ(10u, impl.getQueueSize()); + + auto pusher = std::async(std::launch::async, [&impl]{ + impl.push(42); + }); + + EXPECT_EQ(pusher.wait_for(10ms), std::future_status::timeout); + ASSERT_EQ(10u, impl.getQueueSize()); + + const auto value = impl.pop(); + EXPECT_EQ(0, value); + + EXPECT_EQ(pusher.wait_for(1s), std::future_status::ready); + EXPECT_NO_FATAL_FAILURE(pusher.get()); + EXPECT_EQ(10u, impl.getQueueSize()); +} + +TEST(BlockLockQueue, PushMoreThanCapacityDoesntDiscard) +{ + constexpr uint32_t queue_size = 10; + LockQueue impl(queue_size); + ASSERT_EQ(0u, impl.getQueueSize()); + for (uint32_t i = 0; i < queue_size; ++i) + { + impl.push(i); + } + ASSERT_EQ(10u, impl.getQueueSize()); + + auto pusher = std::async(std::launch::async, [&impl]{ + ASSERT_NO_THROW(impl.push(42)); + }); + + EXPECT_EQ(pusher.wait_for(10ms), std::future_status::timeout); + ASSERT_EQ(10u, impl.getQueueSize()); + + for (uint32_t i = 0; i < queue_size; ++i) + { + ASSERT_EQ(i, impl.pop()); + } + + EXPECT_EQ(pusher.wait_for(1s), std::future_status::ready); + ASSERT_EQ(1u, impl.getQueueSize()); + ASSERT_EQ(42, impl.pop()); + EXPECT_NO_FATAL_FAILURE(pusher.get()); +} + +TEST(BlockLockQueue, ClearQueueClearsQueue) +{ + uint32_t queue_size = 10; + LockQueue impl(queue_size); + EXPECT_EQ(0u, impl.getQueueSize()); + for (uint32_t i = 0; i < queue_size; ++i) + { + impl.push(i); + } + EXPECT_EQ(queue_size, impl.getQueueSize()); + impl.clearQueue(); + EXPECT_EQ(0u, impl.getQueueSize()); +} + +TEST(BlockLockQueue, MultiThreadPushQueueAlwaysHasOneElement) +{ + LockQueue queue{1}; + queue.push(-1); + + std::vector> workers; + constexpr int num_workers = 10; + + std::promise all_workers_launched; + std::atomic_size_t num_workers_launched{0}; + + for (int i = 0; i < num_workers; ++i) + { + workers.push_back( + std::async( + std::launch::async, + [&queue, i, &num_workers_launched, &all_workers_launched]() + { + if (++num_workers_launched == num_workers) + { all_workers_launched.set_value(); } + + while (!queue.isTerminated()) + { + queue.push(i); + } + } + ) + ); + } + + EXPECT_EQ(all_workers_launched.get_future().wait_for(1s), std::future_status::ready); + + for (int i = 0; i < 10000; ++i) + { + const size_t queue_size = queue.getQueueSize(); + + if (queue_size != 1) + { + queue.terminate(); + } + + ASSERT_EQ(queue_size, 1); + } + + queue.terminate(); +} + +TEST(BlockLockQueue, DTORTerminates) +{ + std::atomic_int worker_sum = 0; + std::atomic_bool worker_finished = false; + std::future worker; + + { + LockQueue queue(10); + std::promise worker_certainly_launched; + worker = std::async( + std::launch::async, + [&queue, &worker_sum, &worker_certainly_launched, &worker_finished]() + { + worker_certainly_launched.set_value(); + try { worker_sum += queue.pop(); } + catch (TerminatedException& ) {} + + worker_finished = true; + } + ); + + EXPECT_NE(worker_certainly_launched.get_future().wait_for(1s), std::future_status::timeout); + ASSERT_FALSE(worker_finished); + } + + worker.wait(); + ASSERT_EQ(worker_sum, 0); + ASSERT_TRUE(worker_finished); +} + +TEST(BlockLockQueue, Notify) +{ + LockQueue queue(1, {42}); + + std::atomic_bool pusher_finished = false; + + auto pusher = std::async( + std::launch::async, + [&queue, &pusher_finished]() + { + queue.push(100); + pusher_finished = true; + } + ); + + // The 'pusher' must wait while the PushBlocking queue is at max capacity. + EXPECT_EQ(pusher.wait_for(5ms), std::future_status::timeout); + EXPECT_FALSE(pusher_finished); + + struct { + std::atomic_size_t num_started_poppers = 0; + std::atomic_size_t num_terminated_poppers = 0; + std::atomic_size_t num_successful_poppers = 0; + std::promise all_started; + } captured; + + constexpr int num_poppers = 10; + std::vector> poppers; + + for (int i = 0; i < num_poppers; ++i) + { + poppers.push_back( + std::async( + std::launch::async, [&queue, &captured]() + { + if (++captured.num_started_poppers == num_poppers) + { captured.all_started.set_value(); } + // All poppers have certainly been created + + try + { // num_poppers will all try to pop. While the queue is empty, they must sleep + queue.pop(); + ++captured.num_successful_poppers; + } // ... unless the queue is terminated + catch (TerminatedException&) + { ++captured.num_terminated_poppers; } + })); + } + + { + const auto future_status = pusher.wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + ASSERT_TRUE(pusher_finished); + } // All poppers have certainly been created + + + { // And the pusher are certainly finished + const auto future_status = captured.all_started.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + } + + // Initial value and pusher's value are popped + EXPECT_EQ(2, captured.num_successful_poppers); + + // All other poppers are blocked + EXPECT_EQ(0, captured.num_terminated_poppers); + + queue.terminate(); + + for (const auto& popper : poppers) + { + const auto future_status = popper.wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + } + EXPECT_EQ(num_poppers-2, captured.num_terminated_poppers); + EXPECT_EQ(2, captured.num_successful_poppers); +} diff --git a/core/test/test_buffered_consumer_port.cpp b/core/test/test_buffered_consumer_port.cpp new file mode 100644 index 0000000..58947f5 --- /dev/null +++ b/core/test/test_buffered_consumer_port.cpp @@ -0,0 +1,587 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/buffered_consumer_port.h" +#include "superflow/policy.h" +#include "superflow/producer_port.h" + +#include "gtest/gtest.h" + +#include +#include +#include + +using namespace flow; + +constexpr auto Blocking = GetMode::Blocking; +constexpr auto Latched = GetMode::Latched; +constexpr auto Single = ConnectPolicy::Single; +constexpr auto Multi = ConnectPolicy::Multi; + +TEST(BufferedConsumer, operator_bool) +{ + using Consumer = BufferedConsumerPort; + auto consumer = std::make_shared(); + + ASSERT_TRUE(*consumer); + consumer->deactivate(); + ASSERT_FALSE(*consumer); +} + +TEST(BufferedConsumer, deactivate_terminates_buffer) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared(10); + + ASSERT_NO_THROW(producer->connect(consumer)); + ASSERT_EQ(1, producer->numConnections()); + + ASSERT_NO_THROW(producer->send(42)); + ASSERT_NO_THROW(producer->send(84)); + ASSERT_EQ(2, consumer->getQueueSize()); + ASSERT_EQ(2, producer->getStatus().num_transactions); + ASSERT_EQ(0, consumer->getStatus().num_transactions); + + ASSERT_EQ(*consumer->getNext(), 42); + ASSERT_EQ(1, consumer->getQueueSize()); + consumer->deactivate(); + + ASSERT_EQ(consumer->getNext(), std::nullopt); + ASSERT_EQ(1, consumer->getQueueSize()); +} + +TEST(BufferedConsumer, deactivate_does_not_disconnect_but_does_not_receive_either) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_EQ(producer.use_count(), 1); + ASSERT_EQ(consumer.use_count(), 1); + + ASSERT_NO_THROW(consumer->connect(producer)); + + ASSERT_EQ(producer.use_count(), 2); + ASSERT_EQ(consumer.use_count(), 3); + ASSERT_EQ(1, producer->numConnections()); + + ASSERT_NO_THROW(consumer->deactivate()); + ASSERT_TRUE(consumer->isConnected()); + + ASSERT_NO_THROW(producer->send(42)); + ASSERT_EQ(0, consumer->getQueueSize()); + + ASSERT_EQ(1, producer->numConnections()); + + ASSERT_EQ(1, producer->getStatus().num_transactions); + ASSERT_EQ(0, consumer->getStatus().num_transactions); +} + +TEST(BufferedConsumer, receiveData) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_NO_THROW(producer->connect(consumer)); + ASSERT_NO_THROW(producer->send(42)); + int item{0}; + ASSERT_NO_THROW(item = consumer->getNext().value()); + ASSERT_EQ(42, item); +} + +TEST(BufferedConsumer, receiveInvalidData) +{ + { + auto consumer = std::make_shared>(); + + consumer->deactivate(); + + ASSERT_THROW(auto item = consumer->getNext().value(), std::bad_optional_access); + ASSERT_NO_THROW(auto item = consumer->getNext()); + ASSERT_EQ(std::nullopt, consumer->getNext()); + ASSERT_FALSE(consumer->getNext().has_value()); + + // This is ok, but undefined behavior + // const int& cat = *(consumer->getNext()); + } +} + +TEST(BufferedConsumer, receiveLatchedData) +{ + using Producer = ProducerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared>(); + + ASSERT_NO_THROW(producer->connect(consumer)); + ASSERT_NO_THROW(producer->send(42)); + int item{0}; + ASSERT_NO_THROW(item = consumer->getNext().value()); + ASSERT_EQ(42, item); + ASSERT_NO_THROW(item = consumer->getNext().value()); + ASSERT_EQ(42, item); + ASSERT_NO_THROW(producer->send(43)); + ASSERT_NO_THROW(item = consumer->getNext().value()); + ASSERT_EQ(43, item); +} + +TEST(BufferedConsumer, latchedDoesAlsoPop) +{ + using Producer = ProducerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared>(3); + + ASSERT_NO_THROW(producer->connect(consumer)); + + { + int item{0}; + ASSERT_NO_THROW(producer->send(42)); + ASSERT_NO_THROW(item = consumer->getNext().value()); + ASSERT_EQ(42, item); + } + { + int item{0}; + ASSERT_NO_THROW(item = consumer->getNext().value()); + ASSERT_EQ(42, item); + } + { + int item{0}; + ASSERT_NO_THROW(producer->send(43)); + ASSERT_NO_THROW(item = consumer->getNext().value()); + ASSERT_EQ(43, item); + } + { + int item{0}; + ASSERT_NO_THROW(item = consumer->getNext().value()); + ASSERT_EQ(43, item); + } + { + int item{0}; + ASSERT_NO_THROW(producer->send(44)); + ASSERT_NO_THROW(producer->send(45)); + ASSERT_NO_THROW(producer->send(46)); + ASSERT_NO_THROW(item = consumer->getNext().value()); + ASSERT_EQ(44, item); + } +} + +TEST(BufferedConsumer, latchedConsumerPortCanBeDeactivated) +{ + using Producer = ProducerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared>(); + + ASSERT_NO_THROW(producer->connect(consumer)); + + std::atomic_bool was_terminated{false}; + std::thread consumer_thread( + [&]() + { + const auto result = consumer->getNext(); + if (not result) + { was_terminated = true; } + }); + consumer->deactivate(); + consumer_thread.join(); + EXPECT_TRUE(was_terminated); +} + +TEST(BufferedConsumer, iterate) +{ + auto producer = std::make_shared>(); + auto consumer = std::make_shared>(20); + + producer->connect(consumer); + + std::promise all_sent; + + auto worker = std::async( + std::launch::async, + [producer, consumer, &all_sent]() + { + for (int i = 0; i < 10; ++i) + { + producer->send(i); + } + ASSERT_EQ(10, producer->getStatus().num_transactions); + all_sent.set_value(); + } + ); + + { + using namespace std::chrono_literals; + const auto future_status = all_sent.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + } + + std::vector values; + + size_t transactions{0}; + for (const auto& v : *consumer) + { + ASSERT_EQ(++transactions, consumer->getStatus().num_transactions); + ASSERT_EQ(v, static_cast(values.size())); + values.push_back(v); + + if (values.size() >= 10) + { consumer->deactivate(); } + } + ASSERT_EQ(10, consumer->getStatus().num_transactions); + ASSERT_EQ(10, values.size()); +} + +struct A +{ + A() = delete; + A(int i) : value{i} {} + constexpr operator int() const { return value; } + int value; +}; + +TEST(BufferedConsumer, consumeObjectsWithNoDefaultConstructor) +{ + auto producer = std::make_shared>(); + auto consumer = std::make_shared>(20); + + producer->connect(consumer); + + std::promise all_sent; + + auto worker = std::async( + std::launch::async, + [producer, consumer, &all_sent]() + { + for (int i = 0; i < 10; ++i) + { + producer->send(i); + } + + ASSERT_EQ(10, producer->getStatus().num_transactions); + all_sent.set_value(); + } + ); + + { + using namespace std::chrono_literals; + const auto future_status = all_sent.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + } + + std::vector values; + + for (const auto& v : *consumer) + { + ASSERT_EQ(v, static_cast(values.size())); + values.push_back(v); + + if (values.size() >= 10) + { consumer->deactivate(); } + } + ASSERT_EQ(10, values.size()); +} + +TEST(BufferedConsumer, streamOperator) +{ + auto consumer = std::make_shared>(10); + + consumer->receive(42, nullptr); + int result; + EXPECT_TRUE(*consumer >> result); + EXPECT_EQ(42, result); + + consumer->deactivate(); + EXPECT_FALSE(*consumer >> result); + EXPECT_EQ(42, result); +} + +TEST(BufferedConsumer, blockingHasNext) +{ + BufferedConsumerPort consumer(1); + + ASSERT_FALSE(consumer.hasNext()); + + consumer.receive(0, nullptr); + + ASSERT_TRUE(consumer.hasNext()); + + { + int i = consumer.getNext().value(); + } + + ASSERT_FALSE(consumer.hasNext()); +} + +TEST(BufferedConsumer, latchedHasNext) +{ + BufferedConsumerPort consumer(1); + + ASSERT_FALSE(consumer.hasNext()); + + consumer.receive(0, nullptr); + + ASSERT_TRUE(consumer.hasNext()); + + { + int i = consumer.getNext().value(); + } + + ASSERT_TRUE(consumer.hasNext()); +} + +TEST(BufferedConsumer, clearBlocking) +{ + BufferedConsumerPort consumer(1); + + consumer.receive(0, nullptr); + + ASSERT_TRUE(consumer.hasNext()); + + consumer.clear(); + + ASSERT_FALSE(consumer.hasNext()); +} + +TEST(BufferedConsumer, clearLatched) +{ + { + BufferedConsumerPort consumer(1); + + consumer.receive(0, nullptr); + + ASSERT_TRUE(consumer.hasNext()); + + consumer.clear(); + + ASSERT_FALSE(consumer.hasNext()); + } + { + BufferedConsumerPort consumer(1); + consumer.receive(42, nullptr); + ASSERT_TRUE(consumer.hasNext()); + + int item{0}; + ASSERT_NO_THROW(item = consumer.getNext().value()); + ASSERT_EQ(42, item); + + ASSERT_TRUE(consumer.hasNext()); + consumer.clear(); + ASSERT_FALSE(consumer.hasNext()); + } +} + +TEST(BufferedConsumer, conversion) +{ + using Consumer = BufferedConsumerPort; + + const auto bool_producer = std::make_shared>(); + const auto int_producer = std::make_shared>(); + const auto consumer = std::make_shared(1); + + ASSERT_NO_THROW(consumer->connect(bool_producer)); + ASSERT_NO_THROW(consumer->connect(int_producer)); + + int_producer->send(2); + ASSERT_TRUE(consumer->getNext().value_or(false)); + + bool_producer->send(true); + ASSERT_TRUE(consumer->getNext().value_or(false)); +} + +namespace +{ +struct IntClass +{ + std::string name; + int val; + + operator int() const + { + return val; + } +}; +} + +TEST(BufferedConsumerPort, implicit_conversion) +{ + using Consumer = BufferedConsumerPort; + + const auto producer = std::make_shared>(); + const auto c_producer = std::make_shared>(); + const auto consumer = std::make_shared(1); + + ASSERT_NO_THROW(consumer->connect(producer)); + ASSERT_NO_THROW(consumer->connect(c_producer)); + + producer->send(2); + ASSERT_EQ(consumer->getNext().value_or(-1), 2); + + c_producer->send({"hei", 2}); + ASSERT_EQ(consumer->getNext().value_or(-1), 2); +} + +namespace +{ +class Base +{ +public: + virtual ~Base() = default; +}; + +class Derived : public Base +{}; +} + +TEST(BufferedConsumer, upCast) +{ + using Consumer = BufferedConsumerPort; + const auto consumer = std::make_shared(1); + + consumer->receive(Base{}, nullptr); + ASSERT_TRUE(consumer->getNext().has_value()); + ASSERT_FALSE(consumer->hasNext()); + + const auto producer = std::make_shared>(); + ASSERT_NO_THROW(consumer->connect(producer)); + + producer->send({}); + ASSERT_TRUE(consumer->getNext().has_value()); + ASSERT_FALSE(consumer->hasNext()); +} + +TEST(BufferedConsumer, variantReceive) +{ + using Consumer = BufferedConsumerPort>; + Consumer consumer{1}; + + consumer.receive(10, nullptr); + + { + const auto opt_value = consumer.getNext(); + ASSERT_TRUE(opt_value.has_value()); + + const auto& variant = opt_value.value(); + ASSERT_TRUE(std::holds_alternative(variant)); + ASSERT_EQ(std::get(variant), 10); + } + + ASSERT_FALSE(consumer.hasNext()); + + consumer.receive("hei", nullptr); + + { + const auto opt_value = consumer.getNext(); + ASSERT_TRUE(opt_value.has_value()); + + const auto& variant = opt_value.value(); + ASSERT_TRUE(std::holds_alternative(variant)); + ASSERT_EQ(std::get(variant), "hei"); + } +} + +TEST(BufferedConsumer, variantConnect) +{ + using Consumer = BufferedConsumerPort>; + const auto consumer = std::make_shared(1); + + { + const auto producer = std::make_shared>(); + + ASSERT_NO_THROW(consumer->connect(producer)); + producer->disconnect(); + } + + { + const auto producer = std::make_shared>(); + + ASSERT_NO_THROW(consumer->connect(producer)); + producer->disconnect(); + } +} + +TEST(BufferedConsumer, getQueueSize) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared(10); + + ASSERT_NO_THROW(producer->connect(consumer)); + ASSERT_EQ(consumer->getQueueSize(), 0); + ASSERT_NO_THROW(producer->send(42)); + ASSERT_EQ(consumer->getQueueSize(), 1); + int item{0}; + ASSERT_NO_THROW(item = consumer->getNext().value()); + ASSERT_EQ(consumer->getQueueSize(), 0); + ASSERT_EQ(42, item); +} + +TEST(BufferedConsumer, notLeaky) +{ + using namespace std::chrono_literals; + + using Producer = ProducerPort; + using LeakyConsumer = BufferedConsumerPort; + using BlockConsumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + + auto leaky_consumer = std::make_shared(1); + auto block_consumer = std::make_shared(1); + + ASSERT_NO_THROW(producer->connect(leaky_consumer)); + ASSERT_NO_THROW(producer->send(21)); + ASSERT_NO_THROW(producer->send(7)); // We expect leaky consumer to not block the producer. + + ASSERT_EQ(leaky_consumer->getQueueSize(), 1); + ASSERT_EQ(block_consumer->getQueueSize(), 0); // Is not connected yet, so, duh... + + EXPECT_EQ(7, leaky_consumer->getNext().value()); + ASSERT_EQ(leaky_consumer->getQueueSize(), 0); // 21 is forever gone + + ASSERT_NO_THROW(producer->connect(block_consumer)); + + ASSERT_EQ(leaky_consumer->getQueueSize(), 0); + ASSERT_EQ(block_consumer->getQueueSize(), 0); + + ASSERT_NO_THROW(producer->send(42)); + + using namespace std::chrono_literals; + std::promise producer_sent; + + auto pusher = std::async(std::launch::async, [&producer, &producer_sent]{ + producer->send(1984); + producer_sent.set_value(); + }); + + ASSERT_EQ(leaky_consumer->getQueueSize(), 1); + ASSERT_EQ(block_consumer->getQueueSize(), 1); + + const auto future = producer_sent.get_future(); + + // producer->send should be stalling now because block_consumer has full buffer + ASSERT_EQ(future.wait_for(10ms), std::future_status::timeout); + + // EXPECT_EQ(1984, leaky_consumer->getNext().value()); + // might or might not be - depends on who is first in line, leaky or blocky consumer... so, cannot test. + + EXPECT_EQ(42, block_consumer->getNext().value()); // this will unblock the pusher + pusher.wait(); + ASSERT_NE(future.wait_for(1s), std::future_status::timeout); + + ASSERT_EQ(1984, leaky_consumer->getNext().value()); // 42 is forever gone + + ASSERT_EQ(leaky_consumer->getQueueSize(), 0); + ASSERT_EQ(block_consumer->getQueueSize(), 1); + + EXPECT_EQ(1984, block_consumer->getNext().value()); + ASSERT_EQ(block_consumer->getQueueSize(), 0); +} diff --git a/core/test/test_callback_consumer_port.cpp b/core/test/test_callback_consumer_port.cpp new file mode 100644 index 0000000..3cdcb2b --- /dev/null +++ b/core/test/test_callback_consumer_port.cpp @@ -0,0 +1,189 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/callback_consumer_port.h" +#include "superflow/policy.h" +#include "superflow/producer_port.h" + +#include "gtest/gtest.h" + +#include +#include + +using namespace flow; + +TEST(CallbackConsumerPort, DataIsReceived) +{ + using Producer = ProducerPort; + + std::promise promise; + + auto producer = std::make_shared(); + auto consumer = std::make_shared>( + [&promise](int i) + { promise.set_value(i); }); + + ASSERT_NO_THROW(producer->connect(consumer)); + + constexpr int value = 42; + producer->send(value); + + ASSERT_EQ(value, promise.get_future().get()); +} + +namespace +{ +class Base +{ +public: + virtual ~Base() = default; +}; + +class Derived : public Base +{}; +} + +TEST(CallbackConsumer, upCast) +{ + using Consumer = CallbackConsumerPort; + bool got_base; + + const auto consumer = std::make_shared( + [&got_base](const Base& b) + { + got_base = true; + } + ); + + got_base = false; + consumer->receive(Base{}, nullptr); + ASSERT_TRUE(got_base); + + const auto producer = std::make_shared>(); + consumer->connect(producer); + + got_base = false; + producer->send(Derived{}); + ASSERT_TRUE(got_base); +} + +TEST(CallbackConsumer, variantReceive) +{ + using Consumer = CallbackConsumerPort>; + + int received_int; + std::string received_string; + + Consumer consumer{ + [&received_int, &received_string](const auto& variant) + { + if (std::holds_alternative(variant)) + { + received_int = std::get(variant); + } + else if (std::holds_alternative(variant)) + { + received_string = std::get(variant); + } + } + }; + + { + received_int = -1; + received_string = ""; + consumer.receive(10, nullptr); + ASSERT_EQ(received_int, 10); + ASSERT_EQ(received_string, ""); + } + + { + received_int = -1; + received_string = ""; + consumer.receive("hei", nullptr); + ASSERT_EQ(received_int, -1); + ASSERT_EQ(received_string, "hei"); + } +} + +TEST(CallbackConsumer, variantConnect) +{ + using Consumer = CallbackConsumerPort>; + + int received_int; + std::string received_string; + + const auto consumer = std::make_shared( + [&received_int, &received_string](const auto& variant) + { + if (std::holds_alternative(variant)) + { + received_int = std::get(variant); + } + else if (std::holds_alternative(variant)) + { + received_string = std::get(variant); + } + } + ); + + { + const auto producer = std::make_shared>(); + + ASSERT_NO_THROW(consumer->connect(producer)); + producer->disconnect(); + } + + { + const auto producer = std::make_shared>(); + + ASSERT_NO_THROW(consumer->connect(producer)); + producer->disconnect(); + } +} + +TEST(CallbackConsumer, numTransactions) +{ + using Producer = ProducerPort; + using Consumer = CallbackConsumerPort; + + std::promise promise; + std::mutex mu; + std::condition_variable cv; + std::atomic_bool tests_verified = false; + + auto producer = std::make_shared(); + auto consumer = std::make_shared( + [&promise, &mu, &cv, &tests_verified](int i) + { + std::unique_lock mlock(mu); + cv.wait(mlock, [&tests_verified](){ return tests_verified.load(); }); + promise.set_value(i); + } + ); + + ASSERT_NO_THROW(producer->connect(consumer)); + + ASSERT_EQ(0, producer->getStatus().num_transactions); + ASSERT_EQ(0, consumer->getStatus().num_transactions); + + constexpr int value = 42; + auto pusher = std::async( + std::launch::async, + [&producer,&value]() + { + ASSERT_NO_THROW(producer->send(value)); + } + ); + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + EXPECT_EQ(1, producer->getStatus().num_transactions); + EXPECT_EQ(0, consumer->getStatus().num_transactions); + + { + std::unique_lock mlock(mu); + tests_verified = true; + cv.notify_one(); + } + + pusher.wait(); + ASSERT_EQ(value, promise.get_future().get()); + EXPECT_EQ(1, consumer->getStatus().num_transactions); +} \ No newline at end of file diff --git a/core/test/test_connection_manager.cpp b/core/test/test_connection_manager.cpp new file mode 100644 index 0000000..7468c71 --- /dev/null +++ b/core/test/test_connection_manager.cpp @@ -0,0 +1,261 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "connectable_port.h" +#include "multi_connectable_port.h" + +#include "superflow/connection_manager.h" +#include "superflow/policy.h" + +#include "gtest/gtest.h" + +using namespace flow; +constexpr auto Multi = ConnectPolicy::Multi; +constexpr auto Single = ConnectPolicy::Single; + +TEST(ConnectionManager, connectSingle) +{ + auto lhs = std::make_shared>(); + auto rhs = std::make_shared>(); + + ConnectionManager connection_manager; + + ASSERT_NO_THROW(connection_manager.connect(lhs, rhs)); + ASSERT_TRUE(connection_manager.isConnected()); + ASSERT_TRUE(lhs->isConnected()); + ASSERT_TRUE(rhs->isConnected()); + ASSERT_EQ(lhs->connection_, rhs); + ASSERT_EQ(rhs->connection_, lhs); +} + +TEST(ConnectionManager, mismatchThrowsSingle) +{ + auto lhs = std::make_shared>(); + auto rhs = std::make_shared>(); + + ConnectionManager connection_manager; + + ASSERT_THROW(connection_manager.connect(lhs, rhs), std::invalid_argument); + ASSERT_FALSE(connection_manager.isConnected()); + ASSERT_FALSE(lhs->isConnected()); + ASSERT_FALSE(rhs->isConnected()); +} + +TEST(ConnectionManager, disconnectSingle) +{ + auto lhs = std::make_shared>(); + auto rhs = std::make_shared>(); + + ConnectionManager connection_manager; + + ASSERT_NO_THROW(connection_manager.connect(lhs, rhs)); + ASSERT_NO_THROW(connection_manager.disconnect(lhs)); + ASSERT_FALSE(connection_manager.isConnected()); + ASSERT_FALSE(lhs->isConnected()); + ASSERT_FALSE(rhs->isConnected()); +} + + +TEST(ConnectionManager, connectAfterdisconnectSingle) +{ + auto lhs = std::make_shared>(); + auto rhs = std::make_shared>(); + + ConnectionManager connection_manager; + + ASSERT_NO_THROW(connection_manager.connect(lhs, rhs)); + ASSERT_EQ(1, connection_manager.getNumConnections()); + ASSERT_NO_THROW(connection_manager.disconnect(lhs)); + ASSERT_EQ(0, connection_manager.getNumConnections()); + + ASSERT_FALSE(connection_manager.isConnected()); + ASSERT_FALSE(lhs->isConnected()); + ASSERT_FALSE(rhs->isConnected()); + EXPECT_NO_THROW(connection_manager.connect(lhs, rhs)); + ASSERT_TRUE(connection_manager.isConnected()); +} + +TEST(ConnectionManager, specificDisconnectSingle) +{ + auto lhs = std::make_shared>(); + auto rhs = std::make_shared>(); + + ConnectionManager connection_manager; + + ASSERT_NO_THROW(connection_manager.connect(lhs, rhs)); + ASSERT_NO_THROW(connection_manager.disconnect(lhs, rhs)); + ASSERT_FALSE(connection_manager.isConnected()); + ASSERT_FALSE(lhs->isConnected()); + ASSERT_FALSE(rhs->isConnected()); +} + +TEST(ConnectionManager, emptyDisconnectNoThrowSingle) +{ + auto lhs = std::make_shared>(); + auto rhs = std::make_shared>(); + + ConnectionManager connection_manager; + + ASSERT_NO_THROW(connection_manager.disconnect(lhs, rhs)); + ASSERT_NO_THROW(connection_manager.disconnect(lhs)); + ASSERT_FALSE(connection_manager.isConnected()); + ASSERT_FALSE(lhs->isConnected()); + ASSERT_FALSE(rhs->isConnected()); +} + +TEST(ConnectionManager, newConnectThrowsSingle) +{ + auto lhs = std::make_shared>(); + auto rhs1 = std::make_shared>(); + auto rhs2 = std::make_shared>(); + + ConnectionManager connection_manager; + + ASSERT_NO_THROW(connection_manager.connect(lhs, rhs1)); + ASSERT_TRUE(connection_manager.isConnected()); + ASSERT_TRUE(lhs->isConnected()); + ASSERT_TRUE(rhs1->isConnected()); + ASSERT_FALSE(rhs2->isConnected()); + + ASSERT_THROW(connection_manager.connect(lhs, rhs2), std::invalid_argument); +} + +TEST(ConnectionManager, connectMulti) +{ + constexpr size_t num_ports = 10; + + auto lhs = std::make_shared>(); + std::vector>> rhss; + + for (size_t i = 0; i < num_ports; ++i) + { + rhss.push_back(std::make_shared>()); + } + + ConnectionManager connection_manager; + + for (size_t i = 0; i < num_ports; ++i) + { + const auto& rhs = rhss[i]; + + ASSERT_NO_THROW(connection_manager.connect(lhs, rhs)); + ASSERT_TRUE(rhs->isConnected()); + ASSERT_EQ(rhs->getNumConnections(), 1); + } + + ASSERT_EQ(connection_manager.getNumConnections(), num_ports); + ASSERT_EQ(lhs->getNumConnections(), num_ports); +} + +TEST(ConnectionManager, mismatchThrowsMulti) +{ + constexpr size_t num_ports = 10; + + auto lhs = std::make_shared>(); + std::vector>> rhss; + + for (size_t i = 0; i < num_ports; ++i) + { + rhss.push_back(std::make_shared>()); + } + + ConnectionManager connection_manager; + + for (size_t i = 0; i < num_ports; ++i) + { + const auto& rhs = rhss[i]; + + ASSERT_NO_THROW(connection_manager.connect(lhs, rhs)); + ASSERT_TRUE(rhs->isConnected()); + ASSERT_EQ(rhs->getNumConnections(), 1); + } + + ASSERT_EQ(connection_manager.getNumConnections(), num_ports); + ASSERT_EQ(lhs->getNumConnections(), num_ports); + + auto mismatch_port = std::make_shared>(); + ASSERT_THROW(connection_manager.connect(lhs, mismatch_port), std::invalid_argument); + ASSERT_EQ(connection_manager.getNumConnections(), num_ports); + ASSERT_EQ(lhs->getNumConnections(), num_ports); + ASSERT_EQ(mismatch_port->getNumConnections(), 0); + ASSERT_FALSE(mismatch_port->isConnected()); +} + +TEST(ConnectionManager, disconnectAllMulti) +{ + constexpr size_t num_ports = 10; + + auto lhs = std::make_shared>(); + std::vector>> rhss; + + for (size_t i = 0; i < num_ports; ++i) + { + rhss.push_back(std::make_shared>()); + } + + ConnectionManager connection_manager; + + for (size_t i = 0; i < num_ports; ++i) + { + const auto& rhs = rhss[i]; + + connection_manager.connect(lhs, rhs); + } + + ASSERT_EQ(connection_manager.getNumConnections(), num_ports); + ASSERT_EQ(lhs->getNumConnections(), num_ports); + + ASSERT_NO_THROW(connection_manager.disconnect(lhs)); + + ASSERT_EQ(connection_manager.getNumConnections(), 0); + ASSERT_EQ(lhs->getNumConnections(), 0); + + for (const auto& rhs : rhss) + { + ASSERT_EQ(rhs->getNumConnections(), 0); + ASSERT_FALSE(rhs->isConnected()); + } +} + +TEST(ConnectionManager, disconnectSpecificMulti) +{ + constexpr size_t num_ports = 10; + + auto lhs = std::make_shared>(); + std::vector>> rhss; + + for (size_t i = 0; i < num_ports; ++i) + { + rhss.push_back(std::make_shared>()); + } + + ConnectionManager connection_manager; + + for (size_t i = 0; i < num_ports; ++i) + { + const auto& rhs = rhss[i]; + + connection_manager.connect(lhs, rhs); + } + + ASSERT_EQ(connection_manager.getNumConnections(), num_ports); + ASSERT_EQ(lhs->getNumConnections(), num_ports); + + const auto& some_rhs = rhss.back(); + ASSERT_FALSE(some_rhs->did_get_disconnect); + + ASSERT_NO_THROW(connection_manager.disconnect(lhs, some_rhs)); + + ASSERT_EQ(connection_manager.getNumConnections(), num_ports - 1); + ASSERT_EQ(lhs->getNumConnections(), num_ports - 1); + ASSERT_EQ(some_rhs->getNumConnections(), 0); + ASSERT_FALSE(some_rhs->isConnected()); + ASSERT_TRUE(some_rhs->did_get_disconnect); +} + +TEST(ConnectionManager, emptyDisconnectNoThrowMulti) +{ + auto lhs = std::make_shared>(); + + ConnectionManager connection_manager; + + ASSERT_NO_THROW(connection_manager.disconnect(lhs)); +} diff --git a/core/test/test_graph.cpp b/core/test/test_graph.cpp new file mode 100644 index 0000000..921cf27 --- /dev/null +++ b/core/test/test_graph.cpp @@ -0,0 +1,364 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "gtest/gtest.h" +#include "templated_testproxel.h" +#include "superflow/graph.h" + +using namespace flow; + +class TestProxel : public Proxel +{ +public: + TestProxel() + : thread_id_{} + , stop_was_called_{false} + {} + + using Ptr = std::shared_ptr; + + void start() override + { + if (not what_str_.empty()) + { throw std::runtime_error(what_str_); } + run(); + } + + void stop() noexcept override + { stop_was_called_ = true; } + + std::thread::id getThreadId() const + { return thread_id_; } + + bool stopWasCalled() const + { return stop_was_called_; }; + + void setException(const std::string& what) + { what_str_ = what; } + +private: + void run() + { thread_id_ = std::this_thread::get_id(); } + + std::thread::id thread_id_; + bool stop_was_called_; + std::string what_str_; +}; + +struct HijackCerr +{ + HijackCerr() + : cerr_(std::cerr.rdbuf(buffer_.rdbuf())) + {} + + ~HijackCerr() + { std::cerr.rdbuf(cerr_); } + + std::string getString() + { return buffer_.str(); } + +private: + std::stringstream buffer_; + std::streambuf* cerr_; +}; + +TEST(Graph, constructsWithEmptySet) +{ EXPECT_NO_FATAL_FAILURE(Graph{}); } + +TEST(Graph, constructsWithOneProxel) +{ EXPECT_NO_FATAL_FAILURE(Graph({{"id", std::make_shared()}})); } + +TEST(Graph, constructsWithSeveralProxels) +{ + EXPECT_NO_FATAL_FAILURE( + Graph( + { + { "id1", std::make_shared() }, + { "id2", std::make_shared() }, + { "id3", std::make_shared() } + } + )); +} + +TEST(Graph, constructsWithMap) +{ + std::map procs; + procs.emplace("id1", std::make_shared()); + procs.emplace("id2", std::make_shared()); + + EXPECT_NO_FATAL_FAILURE(Graph(std::move(procs))); +} + +TEST(Graph, proxelsAreStartedInSeperateThreads) +{ + TestProxel::Ptr proxel_A{std::make_shared()}; + TestProxel::Ptr proxel_B{std::make_shared()}; + + Graph flow{ + { + {"id_a", proxel_A}, + {"id_b", proxel_B} + } + }; + flow.start(); + flow.stop(); + + EXPECT_NE(std::thread::id{}, proxel_A->getThreadId()); + EXPECT_NE(std::thread::id{}, proxel_B->getThreadId()); + EXPECT_NE(std::this_thread::get_id(), proxel_A->getThreadId()); + EXPECT_NE(std::this_thread::get_id(), proxel_B->getThreadId()); + EXPECT_NE(proxel_A->getThreadId(), proxel_B->getThreadId()); +} + +TEST(Graph, proxelsAreStopped) +{ + TestProxel::Ptr proxel_A{std::make_shared()}; + TestProxel::Ptr proxel_B{std::make_shared()}; + + Graph flow{{{"a", proxel_A}, {"b", proxel_B}}}; + flow.start(); + flow.stop(); + + EXPECT_TRUE(proxel_A->stopWasCalled()); + EXPECT_TRUE(proxel_B->stopWasCalled()); +} + +TEST(Graph, destructorCallsStop) +{ + TestProxel::Ptr proxel_A{std::make_shared()}; + TestProxel::Ptr proxel_B{std::make_shared()}; + + { + Graph flow{{{"a", proxel_A}, {"b", proxel_B}}}; + flow.start(); + } + + EXPECT_TRUE(proxel_A->stopWasCalled()); + EXPECT_TRUE(proxel_B->stopWasCalled()); +} + +TEST(Graph, doublestartThrowsException) +{ + TestProxel::Ptr proxel_A{std::make_shared()}; + TestProxel::Ptr proxel_B{std::make_shared()}; + + Graph flow{{{"a", proxel_A}, {"b", proxel_B}}}; + flow.start(); + EXPECT_THROW(flow.start(), std::runtime_error); + flow.stop(); + EXPECT_NO_THROW(flow.start()); + flow.stop(); +} + +TEST(Graph, addingSameIdTwiceThrows) +{ + Proxel::Ptr proxel_A{std::make_shared()}; + Proxel::Ptr proxel_B{std::make_shared()}; + + Graph flow1; + EXPECT_NO_THROW(flow1.add("id1", std::move(proxel_A))); + EXPECT_THROW(flow1.add("id1", std::move(proxel_B)), std::invalid_argument); +} + +TEST(Graph, connectCompatiblePorts) +{ + Graph flow( + { + {"out", std::make_shared>(42)}, + {"in", std::make_shared>(0)} + } + ); + + ASSERT_NO_THROW(flow.connect("out", "outport", "in", "inport")); +} + +TEST(Graph, connectIncompatiblePortsThrows) +{ + Graph flow( + { + {"out", std::make_shared>(42)}, + {"in", std::make_shared>(0.0)} + } + ); + + ASSERT_THROW(flow.connect("out", "outport", "in", "inport"), std::invalid_argument); +} + +TEST(Graph, invalidConnectThrowsErrorWhichContainsProxelAndPortNames) +{ + const std::string proxel1_name = "proxel1"; + const std::string proxel2_name = "proxel2"; + const std::string proxel1_port_name = "outport"; + const std::string proxel2_port_name = "inport"; + + Graph flow( + { + {proxel1_name, std::make_shared>(42)}, + {proxel2_name, std::make_shared>(0.0)} + } + ); + + std::string what; + + try + { + flow.connect( + proxel1_name, + proxel1_port_name, + proxel2_name, + proxel2_port_name + ); + } + catch (const std::invalid_argument& e) + { + what = e.what(); + } + + ASSERT_NE(what.find(proxel1_name), std::string::npos); + ASSERT_NE(what.find(proxel1_port_name), std::string::npos); + ASSERT_NE(what.find(proxel2_name), std::string::npos); + ASSERT_NE(what.find(proxel2_port_name), std::string::npos); +} + +TEST(Graph, connectNonExistingPortsThrows) +{ + Graph flow( + { + {"out", std::make_shared>(42)}, + {"in", std::make_shared>(0)} + } + ); + + ASSERT_THROW(flow.connect("out", "nonexisting_port", "in", "inport"), std::invalid_argument); +} + +TEST(Graph, valuePropagates) +{ + constexpr int value{42}; + Proxel::Ptr proc_out{std::make_shared>(value)}; + auto proc_in = std::make_shared>(0); + + Graph flow( + { + {"out", proc_out}, + {"in", proc_in} + } + ); + + flow.connect("out", "outport", "in", "inport"); + + flow.start(); + flow.stop(); + + ASSERT_EQ(value, proc_in->getValue()); +} + +TEST(Graph, connectToSelfThrows) +{ + Graph flow( + {{"proxel", std::make_shared>(42)}} + ); + + ASSERT_THROW(flow.connect("proxel", "outport", "proxel", "inport"), std::invalid_argument); +} + +TEST(Graph, handleExceptionWithDefaultLogger) +{ + TestProxel::Ptr proxel_A{std::make_shared()}; + TestProxel::Ptr proxel_B{std::make_shared()}; + + const std::string msg{"mayday"}; + proxel_A->setException(msg); + + Graph flow{{{"a", proxel_A}, {"b", proxel_B}}}; + constexpr bool handle_exceptions = true; + + HijackCerr db_cooper; + + flow.start(handle_exceptions); + flow.stop(); + + { + std::ostringstream os; + os << "Proxel 'a' crashed with exception:\n \"mayday\"\n"; + ASSERT_EQ(os.str(), db_cooper.getString()); + } +} + +TEST(Graph, handleExceptionWithQuietLogger) +{ + TestProxel::Ptr proxel_A{std::make_shared()}; + TestProxel::Ptr proxel_B{std::make_shared()}; + + const std::string msg{"mayday"}; + proxel_A->setException(msg); + + Graph flow{{{"a", proxel_A}, {"b", proxel_B}}}; + constexpr bool handle_exceptions = true; + + HijackCerr db_cooper; + + flow.start(handle_exceptions, flow::Graph::quietCrashLogger); + flow.stop(); + + ASSERT_EQ(std::string{}, db_cooper.getString()); +} + +TEST(Graph, handleExceptionWithCustomLogger) +{ + TestProxel::Ptr proxel_A{std::make_shared()}; + TestProxel::Ptr proxel_B{std::make_shared()}; + + const std::string msg{"mayday"}; + proxel_A->setException(msg); + + std::string what_proxel; + std::string what_msg; + flow::Graph::CrashLogger custom_sink = [&what_proxel, &what_msg](const auto& proxel_name , const auto& what) + { + what_proxel = proxel_name; + what_msg = what; + }; + + Graph flow{{{"a", proxel_A}, {"b", proxel_B}}}; + constexpr bool handle_exceptions = true; + + flow.start(handle_exceptions, custom_sink); + flow.stop(); + + EXPECT_EQ(what_proxel, "a"); + EXPECT_EQ(what_msg, msg); + + EXPECT_TRUE(proxel_A->stopWasCalled()); + EXPECT_TRUE(proxel_B->stopWasCalled()); +} + +TEST(Graph, handleExceptionWithExceptionPtr) +{ + TestProxel::Ptr proxel_A{std::make_shared()}; + TestProxel::Ptr proxel_B{std::make_shared()}; + + const std::string msg{"mayday"}; + proxel_A->setException(msg); + + std::map epq; + flow::Graph::CrashLogger custom_sink = [&epq](const auto& proxel_name , const auto&) + { + epq.emplace(proxel_name, std::current_exception()); + }; + + Graph flow{{{"a", proxel_A}, {"b", proxel_B}}}; + constexpr bool handle_exceptions = true; + + flow.start(handle_exceptions, custom_sink); + flow.stop(); + + ASSERT_FALSE(epq.empty()); + ASSERT_TRUE(epq.count("a") > 0); + try + { + std::rethrow_exception(epq.at("a")); + } + catch (const std::exception& e) + { + EXPECT_EQ(msg, e.what()); + } +} \ No newline at end of file diff --git a/core/test/test_graph_factory.cpp b/core/test/test_graph_factory.cpp new file mode 100644 index 0000000..7b7cb72 --- /dev/null +++ b/core/test/test_graph_factory.cpp @@ -0,0 +1,139 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "templated_testproxel.h" + +#include "superflow/graph_factory.h" + +#include "gtest/gtest.h" + +using namespace flow; + +class TestPropertyList +{ + +public: + using Input = std::map; + + explicit TestPropertyList(Input input) + : input_{std::move(input)} + {} + + bool hasKey(const std::string& key) const + { return input_.count(key) > 0; } + + template + T convertValue(const std::string& key) const + { + if (!hasKey(key)) + { throw std::invalid_argument({"Could not find key '" + key + "' in PropertyList."}); } + + return static_cast(input_.at(key)); + } + +private: + Input input_; +}; + +template +Factory getProxelFactory() +{ + return [](const TestPropertyList& properties) + { + return F::create(properties); + }; +} + +TEST(GraphFactory, createGraph) +{ + using TestProxel = TemplatedProxel; + + const FactoryMap factories( + { + {"TemplatedProxel", getProxelFactory()} + }); + + // for hver ting (sensor, publisher, proxel, ...) + TestPropertyList::Input properties1 = {{"init_value", 42.0}}; + TestPropertyList::Input properties2 = {{"init_value", 21.0}}; + + const auto config = std::vector> { + {"proxel1", "TemplatedProxel", TestPropertyList{std::move(properties1)}}, + {"proxel2", "TemplatedProxel", TestPropertyList{std::move(properties2)}} + }; + + std::vector connections{ + {"proxel1", "outport", "proxel2", "inport"} + }; + + auto graph = createGraph(factories, config, connections); + + Proxel::Ptr raw_ptr = graph.getProxel("proxel1"); + ASSERT_TRUE(raw_ptr != nullptr); + + auto ptr2 = graph.getProxel("proxel2"); + ASSERT_TRUE(ptr2 != nullptr); + ASSERT_EQ(21.0, ptr2->getStoredValue()); + + graph.start(); + ASSERT_EQ(21.0, ptr2->getStoredValue()); + ASSERT_EQ(42.0, ptr2->getValue()); +} + +TEST(GraphFactory, createGraphWithout_getProxelFactory) +{ + using Plist = TestPropertyList; + using MyProxel = TemplatedProxel; + + const Factory factory = MyProxel::create; + const FactoryMap factories{{{"MyProxel", factory}}}; + + const Plist properties1{{{"init_value", 42.0}}}; + const Plist properties2{{{"init_value", 21.0}}}; + + const auto configs = std::vector> { + {"proxel1", "MyProxel", properties1}, + {"proxel2", "MyProxel", properties2} + }; + + const std::vector connections{ + {"proxel1", "outport", "proxel2", "inport"} + }; + + Graph graph = createGraph(factories, configs, connections); + + const Proxel::Ptr raw_ptr = graph.getProxel("proxel1"); + ASSERT_TRUE(raw_ptr != nullptr); + + const auto ptr2 = graph.getProxel("proxel2"); + ASSERT_TRUE(ptr2 != nullptr); + ASSERT_EQ(21.0, ptr2->getStoredValue()); + + graph.start(); + ASSERT_EQ(21.0, ptr2->getStoredValue()); + ASSERT_EQ(42.0, ptr2->getValue()); + +} + +TEST(GraphFactory, multiple_definitions_of_same_proxel_id_throws) +{ + using Plist = TestPropertyList; + using MyProxel = TemplatedProxel; + + const Factory factory = MyProxel::create; + const FactoryMap factories{{{"MyProxel", factory}}}; + + const Plist properties1{{{"init_value", 42.0}}}; + const Plist properties2{{{"init_value", 21.0}}}; + + const auto configs = std::vector> { + {"proxel1", "MyProxel", properties1}, + {"proxel1", "MyProxel", properties1}, + {"proxel2", "MyProxel", properties2} + }; + + const std::vector connections{ + {"proxel1", "outport", "proxel2", "inport"} + }; + + ASSERT_THROW(createGraph(factories, configs, connections), std::invalid_argument); +} + diff --git a/core/test/test_interface_port.cpp b/core/test/test_interface_port.cpp new file mode 100644 index 0000000..3d60e2f --- /dev/null +++ b/core/test/test_interface_port.cpp @@ -0,0 +1,107 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/interface_port.h" + +#include "gtest/gtest.h" + +using namespace flow; + +class MyInterface +{ +public: + virtual int foo(int bar) const = 0; +}; + +class MyHost : public MyInterface +{ +public: + MyHost() + : port{std::make_shared::Host>(*this)} + {} + + int foo(const int bar) const override + { + return 2*bar; + } + + InterfacePort::Host::Ptr port; +}; + +TEST(InterfacePort, happy_path) +{ + using Client = InterfacePort::Client; + + MyHost host; + const auto client = std::make_shared(); + + client->connect(host.port); + + ASSERT_EQ(2, client->get().foo(1)); + ASSERT_EQ(42, client->get().foo(21)); +} + +TEST(InterfacePort, isConnected) +{ + using Client = InterfacePort::Client; + + MyHost host; + const auto client = std::make_shared(); + + EXPECT_FALSE(host.port->isConnected()); + EXPECT_FALSE(client->isConnected()); + + client->connect(host.port); + + EXPECT_TRUE(host.port->isConnected()); + EXPECT_TRUE(client->isConnected()); +} + +TEST(InterfacePort, getThrowsIfNotConnected) +{ + using Client = InterfacePort::Client; + + MyHost host; + const auto client = std::make_shared(); + + ASSERT_FALSE(host.port->isConnected()); + ASSERT_FALSE(client->isConnected()); + + EXPECT_THROW(host.port->get(), std::runtime_error); + EXPECT_THROW(client->get(), std::runtime_error); + + client->connect(host.port); + + EXPECT_NO_THROW(host.port->get()); + EXPECT_NO_THROW(client->get()); +} + +TEST(InterfacePort, num_transactions) +{ + using Client = InterfacePort::Client; + + MyHost my_host; + const auto& host = my_host.port; + const auto client = std::make_shared(); + + ASSERT_NO_FATAL_FAILURE(client->connect(host)); + ASSERT_EQ(0, host->getStatus().num_transactions); + ASSERT_EQ(0, client->getStatus().num_transactions); + + ASSERT_EQ(2, client->get().foo(1)); + + EXPECT_EQ(1, host->getStatus().num_transactions); + EXPECT_EQ(1, client->getStatus().num_transactions); + + ASSERT_NO_FATAL_FAILURE( client->get()); + + EXPECT_EQ(2, host->getStatus().num_transactions); + EXPECT_EQ(2, client->getStatus().num_transactions); + + client->disconnect(); + EXPECT_THROW(client->get(), std::runtime_error); + EXPECT_EQ(3, client->getStatus().num_transactions); + EXPECT_EQ(2, host->getStatus().num_transactions); + + EXPECT_THROW(host->get(), std::runtime_error); + EXPECT_EQ(3, client->getStatus().num_transactions); + EXPECT_EQ(3, host->getStatus().num_transactions); +} \ No newline at end of file diff --git a/core/test/test_lock_queue.cpp b/core/test/test_lock_queue.cpp new file mode 100644 index 0000000..73a05f5 --- /dev/null +++ b/core/test/test_lock_queue.cpp @@ -0,0 +1,266 @@ +#include "superflow/utils/lock_queue.h" + +#include "gtest/gtest.h" + +#include +#include + +using namespace flow; + +TEST(LockQueue, QueueSizeZeroThrows) +{ + ASSERT_THROW(LockQueue(0), std::invalid_argument); +} + +TEST(LockQueue, PushLvalue) +{ + LockQueue impl(10); + int val = 42; + ASSERT_NO_FATAL_FAILURE(impl.push(val)); +} + +TEST(LockQueue, PushRvalue) +{ + LockQueue impl(10); + ASSERT_NO_FATAL_FAILURE(impl.push(42)); +} + +TEST(LockQueue, Queue_size_from_const_reference_works) +{ + LockQueue impl(10); + const auto& cref = impl; + + ASSERT_EQ(0u, cref.getQueueSize()); +} + +TEST(LockQueue, PushIncreasesQueueSize) +{ + LockQueue impl(10); + ASSERT_EQ(0u, impl.getQueueSize()); + impl.push(42); + ASSERT_EQ(1u, impl.getQueueSize()); +} + +TEST(LockQueue, InitializerListInitializedQueue) +{ + LockQueue impl(10, {42, 2, 3}); + EXPECT_EQ(3u, impl.getQueueSize()); + EXPECT_EQ(42, impl.pop()); +} + +TEST(LockQueue, TooLongInitializerListThrowsException) +{ + EXPECT_THROW(LockQueue(2, {42, 2, 3}), std::range_error); +} + +TEST(LockQueue, PopReturnsInsertedValue) +{ + LockQueue impl(10); + const int val = 42; + + // Test T pop() + impl.push(val); + impl.push(val + 1); + EXPECT_EQ(val, impl.pop()); + EXPECT_EQ(val + 1, impl.pop()); + + // Test void pop(T&) + EXPECT_EQ(0u, impl.getQueueSize()); + impl.push(val); + impl.push(val + 1); + int res = 0; + EXPECT_NO_FATAL_FAILURE(impl.pop(res)); + EXPECT_EQ(val, res); + EXPECT_NO_FATAL_FAILURE(impl.pop(res)); + EXPECT_EQ(val + 1, res); +} + +TEST(LockQueue, PopDecreasesQueueSize) +{ + LockQueue impl(10); + ASSERT_EQ(0u, impl.getQueueSize()); + impl.push(42); + ASSERT_EQ(1u, impl.getQueueSize()); + impl.pop(); + ASSERT_EQ(0u, impl.getQueueSize()); +} + +TEST(LockQueue, PopHangsUntilPushAndDoesNotThrow) +{ + LockQueue impl(10); + int popped_value = 0; + auto push_fn = [&impl]() + { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + impl.push(42); + }; + std::thread pusher(push_fn); + ASSERT_TRUE(pusher.joinable()); + EXPECT_EQ(0, popped_value); + + EXPECT_NO_THROW(popped_value = impl.pop()); + + pusher.join(); + EXPECT_EQ(42, popped_value); +} + +TEST(LockQueue, PopHangsUntilTerminateAndThenThrows) +{ + LockQueue impl(10); + + int popped_value = 42; + + auto term_fn = [&impl]() + { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + EXPECT_NO_THROW(impl.terminate()); + }; + + std::thread terminator(term_fn); + ASSERT_TRUE(terminator.joinable()); + + EXPECT_THROW(popped_value = impl.pop(), TerminatedException); + terminator.join(); + EXPECT_EQ(42, popped_value); +} + +TEST(LockQueue, TerminateTwiceDoesNotThrow) +{ + LockQueue impl(10); + EXPECT_NO_THROW(impl.terminate()); + EXPECT_NO_THROW(impl.terminate()); +} + +TEST(LockQueue, Queue_correctly_responds_if_it_is_terminated_or_not) +{ + std::mutex mu; + std::condition_variable cv; + std::atomic_bool tests_verified = false; + + LockQueue impl(10); + auto terminator = std::async( + std::launch::async, + [&impl, &mu, &cv, &tests_verified]() + { + std::unique_lock mlock(mu); + cv.wait(mlock, [&tests_verified]() + { return tests_verified.load(); }); + impl.terminate(); + } + ); + EXPECT_FALSE(impl.isTerminated()); + { + std::unique_lock mlock(mu); + tests_verified = true; + cv.notify_one(); + } + + terminator.wait(); + EXPECT_TRUE(impl.isTerminated()); +} + +TEST(LockQueue, PushMoreThanCapacityDoesNotIncreasesQueueSize) +{ + uint32_t queue_size = 10; + LockQueue impl(queue_size); + ASSERT_EQ(0u, impl.getQueueSize()); + for (uint32_t i = 0; i < queue_size; ++i) + { + impl.push(i); + } + ASSERT_EQ(10u, impl.getQueueSize()); + impl.push(42); + ASSERT_EQ(10u, impl.getQueueSize()); +} + +TEST(LockQueue, PushMoreThanCapacityDiscardsFront) +{ + uint32_t queue_size = 10; + LockQueue impl(queue_size); + ASSERT_EQ(0u, impl.getQueueSize()); + for (uint32_t i = 0; i < queue_size; ++i) + { + impl.push(i); + } + ASSERT_EQ(10u, impl.getQueueSize()); + impl.push(42); + auto front = impl.pop(); + ASSERT_EQ(1, front); +} + +TEST(LockQueue, ClearQueueClearsQueue) +{ + uint32_t queue_size = 10; + LockQueue impl(queue_size); + EXPECT_EQ(0u, impl.getQueueSize()); + for (uint32_t i = 0; i < queue_size; ++i) + { + impl.push(i); + } + EXPECT_EQ(queue_size, impl.getQueueSize()); + impl.clearQueue(); + EXPECT_EQ(0u, impl.getQueueSize()); +} + +TEST(LockQueue, MultiThreadPushQueueAlwaysHasOneElement) +{ + LockQueue queue{1}; + queue.push(-1); + + bool is_running = true; + + std::vector> workers; + constexpr int num_workers = 10; + + for (int i = 0; i < num_workers; ++i) { + workers.push_back(std::async(std::launch::async, [&is_running, &queue, i]() + { + while (is_running) { + queue.push(i); + } + })); + } + + std::this_thread::sleep_for(std::chrono::milliseconds{10}); + + for (int i = 0; i < 10000; ++i) { + const size_t queue_size = queue.getQueueSize(); + + if (queue_size != 1) { + is_running = false; + } + + ASSERT_EQ(queue_size, 1); + } + + is_running = false; +} + +TEST(LockQueue, DTORTerminates) +{ + std::atomic_int worker_sum = 0; + std::atomic_bool worker_finished = false; + std::future worker; + + { + LockQueue queue(10); + + worker = std::async( + std::launch::async, + [&queue, &worker_sum, &worker_finished]() + { + try { worker_sum += queue.pop(); } + catch (TerminatedException& ) {} + + worker_finished = true; + } + ); + + std::this_thread::sleep_for(std::chrono::milliseconds{5}); + ASSERT_FALSE(worker_finished); + } + + worker.wait(); + ASSERT_EQ(worker_sum, 0); + ASSERT_TRUE(worker_finished); +} diff --git a/core/test/test_metronome.cpp b/core/test/test_metronome.cpp new file mode 100644 index 0000000..be89754 --- /dev/null +++ b/core/test/test_metronome.cpp @@ -0,0 +1,86 @@ +// Copyright (c) 2020 Forsvarets forskningsinstitutt (FFI). All rights reserved. +#include "superflow/utils/metronome.h" + +#include "gtest/gtest.h" + +#include + +using namespace std::chrono_literals; + +TEST(Metronome, check) +{ + { + std::promise promise; + std::atomic_bool promise_isset{false}; + + flow::Metronome non_crashing_repeater{ + [&promise,&promise_isset](const auto) + { + if (promise_isset) + { return ; } + + promise.set_value(); + promise_isset = true; + }, + 1us + }; + + const auto future_status = promise.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + + EXPECT_NO_THROW(non_crashing_repeater.check()); + } + + { + std::promise promise; + flow::Metronome crashing_repeater{ + [&promise](const auto) + { + promise.set_value(); + throw std::runtime_error{"error"}; + }, + std::chrono::microseconds{1} + }; + + const auto future_status = promise.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + std::this_thread::sleep_for(10ms); + + EXPECT_THROW(crashing_repeater.check(), std::runtime_error); + } +} + +TEST(Metronome, get) +{ + { + std::promise promise; + + flow::Metronome metronome{ + [&promise](const auto) + { + static bool once = std::invoke([&promise]{ promise.set_value(); return true; }); + }, + std::chrono::microseconds{1} + }; + + const auto future_status = promise.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + + metronome.stop(); + EXPECT_NO_THROW(metronome.get()); + EXPECT_THROW(metronome.get(), std::runtime_error); + } +} + +TEST(Metronome, stop) +{ + { + flow::Metronome metronome{ + [](auto){}, + 1us + }; + + metronome.stop(); + EXPECT_NO_THROW(metronome.get()); + } +} diff --git a/core/test/test_multi_consumer_port.cpp b/core/test/test_multi_consumer_port.cpp new file mode 100644 index 0000000..5e08321 --- /dev/null +++ b/core/test/test_multi_consumer_port.cpp @@ -0,0 +1,311 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/multi_consumer_port.h" +#include "superflow/producer_port.h" + +#include "gtest/gtest.h" + +#include +#include +#include + +using namespace flow; + +TEST(MultiConsumer, receive) +{ + using Producer = ProducerPort; + + constexpr size_t num_producers = 10; + auto consumer = std::make_shared>(); + + std::vector> producers; + + for (size_t i = 0; i < num_producers; ++i) + { + producers.push_back(std::make_shared()); + } + + for (auto& producer : producers) + { + ASSERT_NO_THROW(producer->connect(consumer)); + ASSERT_NO_THROW(producer->send(42)); + } + + for (size_t i = 0; i < 10; ++i) + { + auto items = consumer->get(); + + for (const auto item : items) + { + ASSERT_EQ(item, 42); + } + } +} + +TEST(MultiConsumer, emptyConsumerReceivesNothing) +{ + auto consumer = std::make_shared>(); + auto items = consumer->get(); + + ASSERT_TRUE(items.empty()); +} + +TEST(MultiConsumer, deactivateCausesThrow) +{ + auto consumer = std::make_shared>(); + auto producer = std::make_shared>(); + + consumer->connect(producer); + consumer->deactivate(); + + ASSERT_THROW(consumer->get(), std::runtime_error); +} + +TEST(MultiConsumer, receiveIsLatched) +{ + using Producer = ProducerPort; + + constexpr size_t num_producers = 10; + auto consumer = std::make_shared>(); + + std::vector> producers; + + for (size_t i = 0; i < num_producers; ++i) + { + producers.push_back(std::make_shared()); + } + + for (auto& producer : producers) + { + ASSERT_NO_THROW(producer->connect(consumer)); + } + + // send data for all but one producer + for (size_t i = 0; i < num_producers - 1; ++i) + { + producers[i]->send(42); + } + + bool got_data = false; + auto worker = std::async(std::launch::async, [&got_data, &consumer]() + { + auto item = consumer->get(); // expected to throw when deactivate() is called + got_data = true; // which means this won't happen + }); + + std::this_thread::sleep_for(std::chrono::milliseconds{10}); + ASSERT_FALSE(got_data); + + consumer->deactivate(); + + ASSERT_THROW(worker.get(), std::runtime_error); + ASSERT_FALSE(got_data); +} + +TEST(MultiConsumer, receiveReadyOnly) +{ + using Producer = ProducerPort; + + constexpr size_t num_producers = 10; + auto consumer = std::make_shared>(); + + std::vector> producers; + + for (size_t i = 0; i < num_producers; ++i) + { + producers.push_back(std::make_shared()); + } + + for (auto& producer : producers) + { + ASSERT_NO_THROW(producer->connect(consumer)); + } + + // send data for all but one producer + for (size_t i = 0; i < num_producers - 1; ++i) + { + producers[i]->send(static_cast(13 * (i + 1))); + } + + const auto ready_items = consumer->get(); + + ASSERT_EQ(ready_items.size(), num_producers - 1); + + for (const int item : ready_items) + { + ASSERT_NE(item, static_cast(13 * num_producers)); + } +} + +TEST(MultiConsumer, deactivateCausesOperatorFalse) +{ + MultiConsumerPort consumer; + + ASSERT_TRUE(consumer); + + consumer.deactivate(); + + ASSERT_FALSE(consumer); +} + +TEST(MultiConsumer, reportsNumConnections) +{ + const auto consumer = std::make_shared>(); + constexpr size_t num_producers = 10; + + for (size_t i = 0; i < num_producers; ++i) + { + consumer->connect(std::make_shared>()); + } + + ASSERT_EQ(consumer->getStatus().num_connections, num_producers); +} + +TEST(MultiConsumer, clear) +{ + const auto consumer = std::make_shared>(2); + + const auto producer1 = std::make_shared>(); + const auto producer2 = std::make_shared>(); + + consumer->connect(producer1); + consumer->connect(producer2); + + ASSERT_FALSE(consumer->hasNext()); + producer1->send(0); + ASSERT_FALSE(consumer->hasNext()); + producer2->send(0); + ASSERT_TRUE(consumer->hasNext()); + consumer->clear(); + ASSERT_FALSE(consumer->hasNext()); + producer1->send(1); + ASSERT_FALSE(consumer->hasNext()); + producer2->send(1); + ASSERT_TRUE(consumer->hasNext()); + + const auto values = consumer->get(); + + ASSERT_EQ(values.size(), 2u); + ASSERT_EQ(values[0], 1); + ASSERT_EQ(values[1], 1); +} + +namespace +{ +class Base +{ +public: + virtual ~Base() = default; +}; + +class Derived : public Base +{}; +} + +TEST(MultiConsumer, upCast) +{ + using Consumer = MultiConsumerPort; + const auto consumer = std::make_shared(1); + + { + const auto producer = std::make_shared>(); + + ASSERT_NO_THROW(consumer->connect(producer)); + + producer->send(Base{}); + ASSERT_TRUE(consumer->getNext().has_value()); + ASSERT_FALSE(consumer->hasNext()); + producer->disconnect(); + } + + { + const auto producer = std::make_shared>(); + + ASSERT_NO_THROW(consumer->connect(producer)); + + producer->send(Derived{}); + ASSERT_TRUE(consumer->getNext().has_value()); + ASSERT_FALSE(consumer->hasNext()); + producer->disconnect(); + } +} + +TEST(MultiConsumer, variantReceive) +{ + using Consumer = MultiConsumerPort>; + Consumer consumer{1}; + + consumer.receive(10, nullptr); + + { + const auto opt_value = consumer.getNext(); + ASSERT_TRUE(opt_value.has_value()); + + const auto& variant = opt_value.value().front(); + ASSERT_TRUE(std::holds_alternative(variant)); + ASSERT_EQ(std::get(variant), 10); + } + + ASSERT_FALSE(consumer.hasNext()); + + consumer.receive("hei", nullptr); + + { + const auto opt_value = consumer.getNext(); + ASSERT_TRUE(opt_value.has_value()); + + const auto& variant = opt_value.value().front(); + ASSERT_TRUE(std::holds_alternative(variant)); + ASSERT_EQ(std::get(variant), "hei"); + } +} + +TEST(MultiConsumer, variantConnect) +{ + using Consumer = MultiConsumerPort>; + const auto consumer = std::make_shared(1); + + { + const auto producer = std::make_shared>(); + + ASSERT_NO_THROW(consumer->connect(producer)); + producer->disconnect(); + } + + { + const auto producer = std::make_shared>(); + + ASSERT_NO_THROW(consumer->connect(producer)); + producer->disconnect(); + } +} + +TEST(MultiConsumer, numTransactions) +{ + using Producer = ProducerPort; + using Consumer = MultiConsumerPort; + + constexpr size_t num_producers = 10; + auto consumer = std::make_shared(); + + std::vector producers; + + for (size_t i = 0; i < num_producers; ++i) + { + producers.push_back(std::make_shared()); + } + + for (auto& producer : producers) + { + ASSERT_NO_THROW(producer->connect(consumer)); + ASSERT_NO_THROW(producer->send(42)); + ASSERT_EQ(1, producer->getStatus().num_transactions); + } + + ASSERT_EQ(0, consumer->getStatus().num_transactions); + for (size_t i = 0; i < 10; ++i) + { + auto items = consumer->get(); + ASSERT_EQ(i+1, consumer->getStatus().num_transactions); + ASSERT_EQ(1, producers[i]->getStatus().num_transactions); + } +} \ No newline at end of file diff --git a/core/test/test_multi_lock_queue.cpp b/core/test/test_multi_lock_queue.cpp new file mode 100644 index 0000000..f02fe38 --- /dev/null +++ b/core/test/test_multi_lock_queue.cpp @@ -0,0 +1,979 @@ +#include "superflow/utils/multi_lock_queue.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include + +using namespace flow; + +using namespace std::chrono_literals; + +TEST(MultiLockQueue, PushLvalue) +{ + constexpr size_t queue_size = 10; + MultiLockQueue multi_queue{queue_size}; + + constexpr int key = 13; + const int val = 42; + + ASSERT_NO_FATAL_FAILURE(multi_queue.push(key, val)); +} + +TEST(MultiLockQueue, PushRvalue) +{ + constexpr size_t queue_size = 10; + MultiLockQueue multi_queue{queue_size}; + + constexpr int key = 21; + + ASSERT_NO_FATAL_FAILURE(multi_queue.push(key, 42)); +} + +TEST(MultiLockQueue, PushToInitedQueue) +{ + constexpr size_t queue_size = 10; + constexpr int key = 21; + MultiLockQueue multi_queue{queue_size, {key}}; + + ASSERT_NO_FATAL_FAILURE(multi_queue.push(key, 42)); +} + +TEST(MultiLockQueue, PushToMultipleQueues) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (int key = 0; key < 10; ++key) + { + ASSERT_NO_FATAL_FAILURE(multi_queue.push(key, key + val_offset)); + } +} + +TEST(MultiLockQueue, PopReadyReturnsInsertedValues) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (int key = 0; key < 10; ++key) + { + ASSERT_NO_FATAL_FAILURE(multi_queue.push(key, key + val_offset)); + } + + const auto values = multi_queue.popReady(); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PopAtLeastOneReturnsInsertedValues) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (int key = 0; key < 10; ++key) + { + ASSERT_NO_FATAL_FAILURE(multi_queue.push(key, key + val_offset)); + } + + const auto values = multi_queue.popAtLeastOne(); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PopAllReturnsInsertedValues) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (int key = 0; key < 10; ++key) + { + ASSERT_NO_FATAL_FAILURE(multi_queue.push(key, key + val_offset)); + } + + const auto values = multi_queue.popAll(); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PeekReadyReturnsInsertedValues) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (int key = 0; key < 10; ++key) + { + ASSERT_NO_FATAL_FAILURE(multi_queue.push(key, key + val_offset)); + } + + const auto values = multi_queue.peekReady(); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PeekAtLeastOneReturnsInsertedValues) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (int key = 0; key < 10; ++key) + { + ASSERT_NO_FATAL_FAILURE(multi_queue.push(key, key + val_offset)); + } + + const auto values = multi_queue.peekAtLeastOne(); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PeekAllReturnsInsertedValues) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (int key = 0; key < 10; ++key) + { + ASSERT_NO_FATAL_FAILURE(multi_queue.push(key, key + val_offset)); + } + + const auto values = multi_queue.peekAll(); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PopAllDoesNotBlockForUninitedQueues) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + std::vector> workers; + std::atomic_size_t finished_workers{0}; + + std::promise unblock; + const auto blocker = unblock.get_future().share(); + + std::promise worker_promise; + const auto workers_done = worker_promise.get_future(); + + for (int key = 0; key < 10; ++key) + { + workers.push_back( + std::async( + std::launch::async, + [key, &multi_queue, &blocker, &worker_promise, &finished_workers]() { + blocker.wait(); + multi_queue.push(key, key + val_offset); + if (++finished_workers == 10) + { + worker_promise.set_value(); + } + })); + } + + ASSERT_EQ(multi_queue.getNumQueues(), 0); + + { + const auto values = multi_queue.popAll(); + + ASSERT_TRUE(values.empty()); + } + + unblock.set_value(); + workers_done.wait(); + + { + const auto values = multi_queue.popAll(); + + ASSERT_EQ(values.size(), workers.size()); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } + } +} + +TEST(MultiLockQueue, PopAllBlocksForCtorInitedQueues) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size, keys}; + + constexpr int val_offset = 13; + + std::vector> workers; + std::promise unblock; + const auto blocker = unblock.get_future().share(); + + for (const auto key : keys) + { + workers.push_back( + std::async( + std::launch::async, + [key, &blocker, &multi_queue]() { + blocker.wait(); + multi_queue.push(key, key + val_offset); + })); + } + + auto future_values = std::async(std::launch::async, [&multi_queue]{ return multi_queue.popAll(); }); + + ASSERT_EQ(future_values.wait_for(10ms), std::future_status::timeout); + unblock.set_value(); + const auto values = future_values.get(); + + ASSERT_EQ(values.size(), workers.size()); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PopAllBlocksForDynamicallyInitedQueues) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + std::vector> workers; + std::promise unblock; + const auto blocker = unblock.get_future().share(); + + for (const auto key : keys) + { + // push dummy values in order to init queue dynamically + multi_queue.push(key, key + val_offset - 1); + + workers.push_back( + std::async( + std::launch::async, + [key, &blocker, &multi_queue]() + { + blocker.wait(); + multi_queue.push(key, key + val_offset); + } + ) + ); + } + + // pop dummy values + multi_queue.popAll(); + + ASSERT_FALSE(multi_queue.hasAny()); + + auto future_values = std::async(std::launch::async, [&multi_queue]{ return multi_queue.popAll(); }); + ASSERT_EQ(future_values.wait_for(10ms), std::future_status::timeout); + + unblock.set_value(); + const auto values = future_values.get(); + + ASSERT_EQ(values.size(), workers.size()); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PopAllRemovesElements) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (const auto key : keys) + { + multi_queue.push(key, key + val_offset - 1); + } + + ASSERT_TRUE(multi_queue.hasAny()); + + // pop dummy values + multi_queue.popAll(); + + ASSERT_FALSE(multi_queue.hasAny()); +} + +TEST(MultiLockQueue, PopAtLeastOneBlocksForFirstValueForCtorInitedQueues) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size, keys}; + + constexpr int val_offset = 13; + + std::promise unblock; + const auto blocker = unblock.get_future().share(); + + const auto worker = std::async( + std::launch::async, + [keys, &blocker, &multi_queue]() + { + blocker.wait(); + multi_queue.push(keys.front(), keys.front() + val_offset); + } + ); + + + auto future_values = std::async(std::launch::async, [&multi_queue]{ return multi_queue.popAtLeastOne(); }); + ASSERT_EQ(future_values.wait_for(10ms), std::future_status::timeout); + + unblock.set_value(); + const auto values = future_values.get(); + ASSERT_EQ(values.size(), 1); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PopAtLeastOneReturnsForNewValue) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + std::vector> workers; + + std::promise unblock; + const auto blocker = unblock.get_future().share(); + + for (const auto key : keys) + { + // push dummy values in order to init queue + multi_queue.push(key, key + val_offset - 1); + + workers.push_back( + std::async( + std::launch::async, + [key, &blocker, &multi_queue]() + { + blocker.wait(); + multi_queue.push(key, key + val_offset); + } + ) + ); + } + + // pop dummy values + multi_queue.popAll(); + ASSERT_FALSE(multi_queue.hasAny()); + + auto future_values = std::async(std::launch::async, [&multi_queue]{ return multi_queue.popAtLeastOne(); }); + ASSERT_EQ(future_values.wait_for(10ms), std::future_status::timeout); + unblock.set_value(); + + const auto values = future_values.get(); + ASSERT_FALSE(values.empty()); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } + +} + +TEST(MultiLockQueue, PopAtLeastOneBlocksForAtLeastOneNewValue) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (const auto key : keys) + { + // push dummy values in order to init queue + multi_queue.push(key, key + val_offset - 1); + } + + // pop dummy values + multi_queue.popAll(); + + multi_queue.push(0, val_offset); + + const auto values = multi_queue.popAtLeastOne(); + + ASSERT_EQ(values.size(), 1); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PopAtLeastOneReturnsAllNewValues) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (const auto key : keys) + { + // push dummy values in order to init queue + multi_queue.push(key, key + val_offset - 1); + } + + // pop dummy values + multi_queue.popAll(); + + constexpr int num_new = 4; + for (int key = 0; key < num_new; ++key) + { + multi_queue.push(key, key + val_offset); + } + + const auto values = multi_queue.popAtLeastOne(); + + ASSERT_EQ(values.size(), static_cast(num_new)); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PopAtLeastOneRemovesElements) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (const auto key : keys) + { + multi_queue.push(key, key + val_offset - 1); + } + + ASSERT_TRUE(multi_queue.hasAny()); + + // pop dummy values + multi_queue.popAtLeastOne(); + + ASSERT_FALSE(multi_queue.hasAny()); +} + +TEST(MultiLockQueue, PopReadyDoesNotBlockForUninitedQueues) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + std::vector> workers; + + std::promise unblock; + const auto blocker = unblock.get_future().share(); + + for (int key = 0; key < 10; ++key) + { + workers.push_back( + std::async( + std::launch::async, + [key, &blocker, &multi_queue]() + { + blocker.wait(); + multi_queue.push(key, key + val_offset); + } + ) + ); + } + + const auto values = multi_queue.popReady(); + ASSERT_TRUE(values.empty()); + unblock.set_value(); +} + +TEST(MultiLockQueue, PopReadyDoesNotBlocksForCtorInitedQueues) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size, keys}; + + constexpr int val_offset = 13; + + std::vector> workers; + + std::promise unblock; + const auto blocker = unblock.get_future().share(); + + for (const auto key : keys) + { + workers.push_back( + std::async( + std::launch::async, + [key, &blocker, &multi_queue]() + { + blocker.wait(); + multi_queue.push(key, key + val_offset); + } + ) + ); + } + + const auto values = multi_queue.popReady(); + ASSERT_TRUE(values.empty()); + unblock.set_value(); +} + +TEST(MultiLockQueue, PopReadyReturnsAllNewValues) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (const auto key : keys) + { + multi_queue.push(key, key + val_offset); + } + + const auto values = multi_queue.popReady(); + + ASSERT_EQ(values.size(), num_keys); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } +} + +TEST(MultiLockQueue, PopReadyRemovesElements) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (const auto key : keys) + { + multi_queue.push(key, key + val_offset - 1); + } + + ASSERT_TRUE(multi_queue.hasAny()); + + // pop dummy values + multi_queue.popReady(); + + ASSERT_FALSE(multi_queue.hasAny()); +} + +TEST(MultiLockQueue, peekAll) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size, keys}; + + constexpr int val_offset = 13; + + std::vector> workers; + + std::promise unblock; + const auto blocker = unblock.get_future().share(); + + for (const auto key : keys) + { + workers.push_back( + std::async( + std::launch::async, + [key, &blocker, &multi_queue]() + { + blocker.wait(); + multi_queue.push(key, key + val_offset); + } + ) + ); + } + + auto future_values = std::async(std::launch::async, [&multi_queue]{ return multi_queue.peekAll(); }); + ASSERT_EQ(future_values.wait_for(10ms), std::future_status::timeout); + + unblock.set_value(); + const auto values = future_values.get(); + ASSERT_EQ(values.size(), workers.size()); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } + + ASSERT_TRUE(multi_queue.hasAll()); +} + +TEST(MultiLockQueue, peekAtLeastOne) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size, keys}; + + constexpr int val_offset = 13; + + std::promise unblock; + const auto blocker = unblock.get_future().share(); + + const auto worker = std::async( + std::launch::async, + [keys, &blocker, &multi_queue]() + { + blocker.wait(); + multi_queue.push(keys.front(), keys.front() + val_offset); + } + ); + + auto future_values = std::async(std::launch::async, [&multi_queue]{ return multi_queue.peekAtLeastOne(); }); + ASSERT_EQ(future_values.wait_for(10ms), std::future_status::timeout); + unblock.set_value(); + + const auto values = future_values.get(); + ASSERT_EQ(values.size(), 1); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } + + ASSERT_TRUE(multi_queue.hasAny()); +} + +TEST(MultiLockQueue, peekReady) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size, keys}; + + constexpr int val_offset = 13; + + { + const auto values = multi_queue.peekReady(); + ASSERT_TRUE(values.empty()); + } + + constexpr int num_new = 4; + + for (int key = 0; key < num_new; ++key) + { + multi_queue.push(key, key + val_offset); + } + + const auto values = multi_queue.peekReady(); + + ASSERT_EQ(values.size(), num_new); + + for (const auto& kv : values) + { + const auto key = kv.first; + const auto val = kv.second; + + ASSERT_EQ(val, key + val_offset); + } + + ASSERT_TRUE(multi_queue.hasAny()); +} + +TEST(MultiLockQueue, ClearClearsAllQueues) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + constexpr int val_offset = 13; + + for (const auto key : keys) + { + multi_queue.push(key, key + val_offset - 1); + } + + ASSERT_TRUE(multi_queue.hasAll()); + ASSERT_EQ(multi_queue.peekAll().size(), num_keys); + + multi_queue.clear(); + + ASSERT_FALSE(multi_queue.hasAny()); + ASSERT_EQ(multi_queue.peekReady().size(), 0); +} + +TEST(MultiLockQueue, DontBlockWhenAllQueuesRemoved) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size, keys}; + + multi_queue.removeAllQueues(); + ASSERT_TRUE(multi_queue.hasAll()); + + // should not block + multi_queue.popAll(); +} + +TEST(MultiLockQueue, DontBlockForRemovedQueues) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size, keys}; + + constexpr size_t num_remove_queues = 3; + + for (const auto key : keys) + { + if (static_cast(key) < num_remove_queues) + { + multi_queue.removeQueue(key); + } else + { + multi_queue.push(key, key) ; + } + } + + ASSERT_TRUE(multi_queue.hasAll()); + + const auto values = multi_queue.peekAll(); + ASSERT_EQ(values.size(), static_cast(num_keys - num_remove_queues)); + + for (const auto kv : values) + { + const auto key = kv.first; + + ASSERT_GE(key, num_remove_queues); + } +} + +TEST(MultiLockQueue, addQueue) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + ASSERT_TRUE(multi_queue.hasAll()); + + multi_queue.addQueue(0); + + ASSERT_FALSE(multi_queue.hasAll()); +} + +TEST(MultiLockQueue, terminateThrowsOnPopAndPeek) +{ + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size}; + + ASSERT_FALSE(multi_queue.isTerminated()); + + multi_queue.terminate(); + + ASSERT_TRUE(multi_queue.isTerminated()); + + ASSERT_THROW(multi_queue.peekReady(), TerminatedException); + ASSERT_THROW(multi_queue.peekAtLeastOne(), TerminatedException); + ASSERT_THROW(multi_queue.peekAll(), TerminatedException); + + ASSERT_THROW(multi_queue.popReady(), TerminatedException); + ASSERT_THROW(multi_queue.popAtLeastOne(), TerminatedException); + ASSERT_THROW(multi_queue.popAll(), TerminatedException); +} + +TEST(MultiLockQueue, QueuesRespectMaxQueueSize) +{ + constexpr size_t num_keys = 10; + std::vector keys(num_keys); + std::iota(keys.begin(), keys.end(), 0); + + constexpr size_t queue_size = 4; + MultiLockQueue multi_queue{queue_size, keys}; + + for (size_t i = 0; i < queue_size + 1; ++i) + { + for (const auto key : keys) + { + multi_queue.push(key, 0); + } + } + + for (size_t i = 0; i < queue_size; ++i) + { + ASSERT_TRUE(multi_queue.hasAll()); + + const auto values = multi_queue.popAll(); + + ASSERT_EQ(values.size(), num_keys); + } + + ASSERT_FALSE(multi_queue.hasAny()); +} + +TEST(MultiLockQueue, AddExistingQueueDoesNotClear) +{ + const std::vector keys = {0}; + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size, keys}; + + multi_queue.push(keys.front(), 0); + + ASSERT_TRUE(multi_queue.hasAll()); + + multi_queue.addQueue(keys.front()); + + ASSERT_TRUE(multi_queue.hasAll()); +} + +TEST(MultiLockQueue, RemoveNonExistingQueueDoesNothing) +{ + const std::vector keys = {0}; + + constexpr size_t queue_size = 1; + MultiLockQueue multi_queue{queue_size, keys}; + + multi_queue.push(keys.front(), 0); + + ASSERT_TRUE(multi_queue.hasAll()); + + constexpr int non_existing_key = 42; + ASSERT_NO_THROW(multi_queue.removeQueue(non_existing_key)); + + ASSERT_TRUE(multi_queue.hasAll()); +} diff --git a/core/test/test_multi_queue_getter.cpp b/core/test/test_multi_queue_getter.cpp new file mode 100644 index 0000000..bf03599 --- /dev/null +++ b/core/test/test_multi_queue_getter.cpp @@ -0,0 +1,47 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/multi_queue_getter.h" + +#include "gtest/gtest.h" + +using namespace flow; + +TEST(MultiQueueGetter, LatchedPopsQueuesWithMultipleElements) +{ + MultiLockQueue multi_queue(2, {0, 1}); + MultiQueueGetter getter; + + // push elements `42` and `13` into queue 0 + multi_queue.push(0, 42); + multi_queue.push(0, 13); + + // only push element `42` into queue 1 + multi_queue.push(1, 42); + + { + std::vector values; + getter.get(multi_queue, values); + + // expect both to be 42 + ASSERT_EQ(values[0], 42); + ASSERT_EQ(values[1], 42); + } + + { + std::vector values; + getter.get(multi_queue, values); + + // expect one value (the one from queue 0) to be 13, and the other to be 42 + // QueueGetter does not guarantee the order, so we don't know which is which + ASSERT_EQ(std::max(values[0], values[1]), 42); + ASSERT_EQ(std::min(values[0], values[1]), 13); + } + + { + std::vector values; + getter.get(multi_queue, values); + + // still expect one value (the one from queue 0) to be 13, and the other to be 42 + ASSERT_EQ(std::max(values[0], values[1]), 42); + ASSERT_EQ(std::min(values[0], values[1]), 13); + } +} diff --git a/core/test/test_multi_requester_port.cpp b/core/test/test_multi_requester_port.cpp new file mode 100644 index 0000000..c5ca8a9 --- /dev/null +++ b/core/test/test_multi_requester_port.cpp @@ -0,0 +1,202 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/multi_requester_port.h" +#include "superflow/responder_port.h" + +#include "gtest/gtest.h" + +#include + +using namespace flow; + +TEST(MultiRequester, request) +{ + using Responder = ResponderPort; + + constexpr size_t num_responders = 10; + auto requester = std::make_shared>(); + + std::vector> responders; + + for (size_t i = 0; i < num_responders; ++i) + { + responders.push_back(std::make_shared( + [](const int v) + { return 2 * v; }) + ); + } + + for (auto& responder : responders) + { + ASSERT_NO_THROW(responder->connect(requester)); + } + + constexpr int query = 23; + const auto responses = requester->request(query); + + for (const auto& response : responses) + { + ASSERT_EQ(response, 2 * query); + } +} + +TEST(MultiRequester, typeMismatchThrows) +{ + auto requester = std::make_shared>(); + auto responder = std::make_shared>( + [](const std::string&) + { return 0; } + ); + + ASSERT_THROW(requester->connect(responder), std::invalid_argument); + ASSERT_THROW(responder->connect(requester), std::invalid_argument); +} + +TEST(MultiRequester, emptyRequesterReceivesNothing) +{ + auto requester = std::make_shared>(); + + std::vector responses; + ASSERT_NO_THROW(responses = requester->request(1)); + ASSERT_TRUE(responses.empty()); +} + +TEST(MultiRequester, responseOrderIsConserved) +{ + using Responder = ResponderPort; + + constexpr size_t num_responders = 10; + auto requester = std::make_shared>(); + + std::vector> responders; + + for (size_t i = 0; i < num_responders; ++i) + { + responders.push_back(std::make_shared( + [i](const int v) + { + return static_cast((i + 1) * v); + }) + ); + } + + for (auto& responder : responders) + { + ASSERT_NO_THROW(responder->connect(requester)); + } + + constexpr int query = 23; + const auto first_responses = requester->request(query); + + const auto second_responses = requester->request(2 * query); + + for (size_t i = 0; i < num_responders; ++i) + { + ASSERT_EQ(2 * first_responses[i], second_responses[i]); + } +} + +template +bool is_ready(const std::future& fu) +{ return fu.valid() && fu.wait_for(std::chrono::seconds(0)) == std::future_status::ready; } + +TEST(MultiRequester, async_request) +{ + using Requester = MultiRequesterPort; + using Responder = ResponderPort; + + constexpr size_t num_responders{5}; + auto requester = std::make_shared(); + + std::vector> responders; + for (size_t i = 0; i < num_responders; ++i) + { + responders.push_back( + std::make_shared([i](const std::string& str) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + return static_cast((i + 1) * str.size()); + }) + ); + } + + for (auto& responder : responders) + { ASSERT_NO_THROW(responder->connect(requester)); } + + const std::string str{"42"}; + auto futures = requester->requestAsync(str); + EXPECT_EQ(futures.size(), num_responders); + + for (const auto& fu : futures) + { + EXPECT_FALSE(is_ready(fu)); + + if (fu.valid()) + { + const auto status = fu.wait_for(std::chrono::seconds(0)); + EXPECT_EQ(status, std::future_status::timeout); + } + } + + for (size_t i = 0; i < futures.size(); ++i) + { + EXPECT_EQ(futures[i].get(), (i+1) * str.size()); + } +} + +TEST(MultiRequester, void) +{ + using Responder = ResponderPort; + + constexpr size_t num_responders = 10; + auto requester = std::make_shared>(); + + std::vector> responders; + responders.reserve(num_responders); + + for (size_t i = 0; i < num_responders; ++i) + { + responders.push_back(std::make_shared([](){})); + } + + for (auto& responder : responders) + { + ASSERT_NO_THROW(responder->connect(requester)); + } + + ASSERT_NO_THROW(requester->request()); +} + +TEST(MultiRequester, numTransactions) +{ + using Responder = ResponderPort; + + constexpr size_t num_responders = 10; + auto requester = std::make_shared>(); + + std::vector responders; + + for (size_t i = 0; i < num_responders; ++i) + { + responders.push_back(std::make_shared( + [](const int v) + { return 2 * v; }) + ); + } + + for (auto& responder : responders) + { + ASSERT_NO_THROW(responder->connect(requester)); + ASSERT_EQ(0, responder->getStatus().num_transactions); + } + + ASSERT_EQ(0, requester->getStatus().num_transactions); + + constexpr int query = 23; + const auto responses = requester->request(query); + ASSERT_EQ(1, requester->getStatus().num_transactions); + + for (auto& responder : responders) + { + ASSERT_EQ(1, responder->getStatus().num_transactions); + } +} \ No newline at end of file diff --git a/core/test/test_mutexed.cpp b/core/test/test_mutexed.cpp new file mode 100644 index 0000000..e1431bc --- /dev/null +++ b/core/test/test_mutexed.cpp @@ -0,0 +1,392 @@ +#include "superflow/utils/mutexed.h" +#include "superflow/utils/blocker.h" +#include "gtest/gtest.h" + +#include + + +TEST(Mutexed, conforms_to_cpp_named_requirement_Lockable) +{ + class A{}; + const flow::Mutexed mutexed; + + EXPECT_NO_FATAL_FAILURE(std::scoped_lock lock{mutexed}); +} + +TEST(Mutexed, mutexed_object_retains_its_properties) +{ + const flow::Mutexed mutexed_str{"42"}; + + const std::string equal{"42"}; + + EXPECT_EQ(mutexed_str, equal); +} + +TEST(Mutexed, example_using_Mutexed_instead_of_adding_a_mutex_to_a_non_threadsafe_type) +{ + struct ThreeVariables + { + int a; + int b; + int c; + }; + + flow::Mutexed threevar; + + { + std::scoped_lock lock{threevar};// protect all variables while writing + threevar.a = 1; + threevar.b = 2; + threevar.c = threevar.a + threevar.b; + } + + int result = 0; + { + std::scoped_lock lock{threevar};// protect all variables while writing + result = threevar.a + threevar.b; + } + + EXPECT_EQ(result, 3); +} + +TEST(Mutexed, assign_other_Mutexed) +{ + flow::Mutexed str1("ice cream"); + flow::Mutexed str2("you scream"); + + ASSERT_NO_FATAL_FAILURE(str1 = str2); + EXPECT_EQ("you scream", str1); +} + +TEST(Mutexed, assign_from_T) +{ + flow::Mutexed str("ice cream"); + { + std::lock_guard mlock(str); + ASSERT_NO_FATAL_FAILURE(str = "you scream"); + } + EXPECT_EQ("you scream", str); +} + +TEST(Mutexed, scoped_lock) +{ + flow::Mutexed str("ice cream"); + { + std::scoped_lock mlock(str); + ASSERT_NO_FATAL_FAILURE(str = "you scream"); + } + EXPECT_EQ("you scream", str); +} + +TEST(Mutexed, move_assign_from_T) +{ + flow::Mutexed mu_str("ice cream"); + { + std::lock_guard mlock(mu_str); + std::string str{"beef"}; + ASSERT_NO_FATAL_FAILURE(mu_str = std::move(str)); + } + EXPECT_EQ("beef", mu_str); +} + +TEST(Mutexed, const_slice) +{ + const flow::Mutexed mu_str("original"); + auto sliced = mu_str.slice(); + sliced = "sliced"; + EXPECT_NE(sliced, mu_str); +} + +TEST(Mutexed, mutable_slice) +{ + flow::Mutexed mu_str("original"); + auto& sliced = mu_str.slice(); + sliced = "sliced"; + EXPECT_EQ(sliced, mu_str); + EXPECT_EQ("sliced", mu_str); +} + +TEST(Mutexed, load) +{ + const flow::Mutexed mu_str("original"); + auto copy = mu_str.load(); + const auto& ref = mu_str.load(); + copy = "a copy"; + mu_str.load() = "temporary discarded, no effect"; + EXPECT_EQ("original", mu_str); + EXPECT_EQ("a copy", copy); + EXPECT_EQ("original", ref); +} + +TEST(Mutexed, load_must_wait) +{ + std::promise promise; + flow::Mutexed mutexed("original"); + std::string value = "some value"; + auto fu = std::async( + std::launch::async, + [&promise, &mutexed, &value] + { + promise.get_future().wait();//< mutexed is locked + + value = mutexed.load();//< will try to lock, so must wait until 'new value' is set + } + ); + + { + std::scoped_lock lock{mutexed}; + promise.set_value(); + ASSERT_NO_FATAL_FAILURE(mutexed = "new value"); + // lock is still in scope, so 'value' is not yet overwritten + EXPECT_EQ("some value", value); + } + + fu.get(); + EXPECT_EQ("new value", value); + EXPECT_EQ("new value", mutexed); +} + +TEST(Mutexed, read_function_call_can_read_value) +{ + const flow::Mutexed mutexed{"original"}; + std::string copied_value; + mutexed.read( + [&copied_value](auto&& mutexed_value) + { copied_value = mutexed_value; } + ); + + EXPECT_EQ("original", copied_value); +} + +TEST(Mutexed, write_function_call_can_overwrite_value) +{ + flow::Mutexed mutexed{"original"}; + mutexed.write( + [](auto&& mutexed_value) + { mutexed_value = "new value"; } + ); + + EXPECT_EQ("new value", mutexed); +} + +TEST(Mutexed, multiple_read_function_calls_are_still_exclusive) +{ + const flow::Mutexed mutexed{"original"}; + + std::promise all_readers_started; + std::atomic_size_t active_readers{0}; + std::atomic_size_t simultaneous_readers{0}; + + std::promise release_readers; + auto must_wait = release_readers.get_future().share(); + + std::vector> readers; + + constexpr size_t num_readers{10}; + for (size_t i{0}; i < num_readers; ++i) + { + readers.emplace_back( + std::async( + std::launch::async, + [&all_readers_started, &must_wait, &active_readers, &simultaneous_readers, &mutexed] + { + if (++active_readers == num_readers) + { all_readers_started.set_value(); } + // All reader threads have certainly been created + + mutexed.read( + [&must_wait, &active_readers, &simultaneous_readers](auto&&) + { + ++simultaneous_readers; + // Should never be more than one + ASSERT_EQ(simultaneous_readers, 1); + must_wait.wait(); + --simultaneous_readers; + --active_readers; + } + ); + } + ) + ); + } + + { // Assert that all readers have certainly been created + using namespace std::chrono_literals; + const auto future_status = all_readers_started.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + + // All are locked out of 'read' except the first. + ASSERT_EQ(active_readers, num_readers); + EXPECT_EQ(simultaneous_readers, 1); + } + + release_readers.set_value(); + + for (auto& reader: readers) + { reader.wait(); } + + ASSERT_EQ(active_readers, 0); + ASSERT_EQ(simultaneous_readers, 0); +} + +TEST(Mutexed, write_must_wait_for_multiple_read) +{ + flow::Mutexed mutexed{"original"}; + + std::promise release_readers; + auto must_wait = release_readers.get_future().share(); + + std::atomic_size_t active_workers{0}; + + std::vector> workers; + constexpr size_t num_readers{10}; + { + std::promise all_readers_started; + for (size_t i{0}; i < num_readers; ++i) + { + workers.emplace_back( + std::async( + std::launch::async, + [&all_readers_started, &must_wait, &active_workers, &mutexed] + { + if (++active_workers == num_readers) + { all_readers_started.set_value(); } + // All reader threads have certainly been created + + mutexed.read( + [&must_wait, &active_workers](auto&&) + { + must_wait.wait(); + --active_workers; + } + ); + } + ) + ); + } + + { // Assert that all readers have certainly been created + using namespace std::chrono_literals; + const auto future_status = all_readers_started.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + + EXPECT_EQ(active_workers, num_readers); + } + } + + { + std::promise writer_started; + workers.emplace_back( + std::async( + std::launch::async, + [&writer_started, &active_workers, &mutexed] + { + ++active_workers; + writer_started.set_value(); + mutexed.write( + [&active_workers](auto&& str) + { str = std::to_string(active_workers); } + ); + --active_workers; + } + ) + ); + + { // Assert that the writer has certainly been created + using namespace std::chrono_literals; + const auto future_status = writer_started.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + + // Readers are still blocked in 'read', so writer is also blocked. + ASSERT_EQ(active_workers, num_readers + 1); + } + } + + release_readers.set_value(); + for (auto& worker: workers) + { worker.wait(); } + + EXPECT_EQ(active_workers, 0); + EXPECT_EQ("1", mutexed); +} + + +TEST(Mutexed, store_must_wait_for_multiple_read) +{ + flow::Mutexed mutexed{"original"}; + + std::promise release_readers; + auto must_wait = release_readers.get_future().share(); + + std::atomic_size_t active_workers{0}; + + std::vector> workers; + constexpr size_t num_readers{10}; + { + std::promise all_readers_started; + for (size_t i{0}; i < num_readers; ++i) + { + workers.emplace_back( + std::async( + std::launch::async, + [&all_readers_started, &must_wait, &active_workers, &mutexed] + { + if (++active_workers == num_readers) + { all_readers_started.set_value(); } + // All reader threads have certainly been created + + mutexed.read( + [&must_wait, &active_workers](auto&&) + { + must_wait.wait(); + --active_workers; + } + ); + } + ) + ); + } + + { // Assert that all readers have certainly been created + using namespace std::chrono_literals; + const auto future_status = all_readers_started.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + + EXPECT_EQ(active_workers, num_readers); + } + } + + { + std::promise writer_started; + workers.emplace_back( + std::async( + std::launch::async, + [&writer_started, &active_workers, &mutexed] + { + ++active_workers; + writer_started.set_value(); + mutexed.store("store"); + mutexed.store(mutexed.load() + "," + std::to_string(active_workers)); + --active_workers; + } + ) + ); + + { // Assert that the writer has certainly been created + using namespace std::chrono_literals; + const auto future_status = writer_started.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + + // when writer is eventually started, + // the first reader is blocked and the rest are locked out + ASSERT_EQ(active_workers, num_readers + 1); + } + } + + release_readers.set_value(); + for (auto& worker: workers) + { worker.wait(); } + + EXPECT_EQ(active_workers, 0); + EXPECT_EQ("store,1", mutexed); +} diff --git a/core/test/test_pimpl.cpp b/core/test/test_pimpl.cpp new file mode 100644 index 0000000..ea39728 --- /dev/null +++ b/core/test/test_pimpl.cpp @@ -0,0 +1,13 @@ +// Copyright (c) 2020 Forsvarets forskningsinstitutt (FFI). All rights reserved. +#include "pimpl_test.h" + +#include "gtest/gtest.h" + +TEST(SuperFlowCurses, pimpl_empty_ctor) +{ + // This works if explicit instantiation is done for the impl-class. + // See pimpl_test.cpp#6. + // There should be no linker errors and no + // "undefined reference to 'pimpl::~pimpl()'" + flow::test::A a; +} \ No newline at end of file diff --git a/core/test/test_port_manager.cpp b/core/test/test_port_manager.cpp new file mode 100644 index 0000000..4a98e5c --- /dev/null +++ b/core/test/test_port_manager.cpp @@ -0,0 +1,90 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/port_manager.h" +#include "superflow/requester_port.h" + +#include "connectable_port.h" + +#include "gtest/gtest.h" + +using namespace flow; + +TEST(PortManager, throwsIfPortDoesNotExistWhenEmpty) +{ + PortManager manager; + Port::Ptr result; + ASSERT_THROW(result = manager.get("does not exist"), std::invalid_argument); +} + +TEST(PortManager, throwsIfPortDoesNotExistWhenNonEmpty) +{ + PortManager manager{{ + {"foo", nullptr} + }}; + + Port::Ptr result; + ASSERT_THROW(result = manager.get("does not exist"), std::invalid_argument); +} + +TEST(PortManager, returnsCorrectPort) +{ + Port::Ptr some_port = std::make_shared>(); + Port::Ptr some_other_port = std::make_shared>(); + + PortManager manager{{ + {"foo", some_port}, + {"bar", some_other_port}, + {"baz", std::make_shared>()} + }}; + + Port::Ptr result; + ASSERT_THROW(result = manager.get("does not exist"), std::invalid_argument); + ASSERT_FALSE(some_port == some_other_port); + ASSERT_EQ(manager.get("foo"), some_port); + ASSERT_EQ(manager.get("bar"), some_other_port); +} + +TEST(PortManager, dtorDisconnectsPorts) +{ + constexpr size_t num_ports = 10; + std::map>> ports; + + for (size_t i = 0; i < num_ports; ++i) + { + std::ostringstream ss; + ss << "port_" << i; + + const std::string port_id = ss.str(); + + ports[port_id] = std::make_shared>(); + ASSERT_FALSE(ports[port_id]->did_get_disconnect); + } + + { + PortManager manager{{ports.begin(), ports.end()}}; + + for (const auto& kv : ports) + { + ASSERT_FALSE(kv.second->did_get_disconnect); + } + } + + for (const auto& kv : ports) + { + ASSERT_TRUE(kv.second->did_get_disconnect); + } +} + +TEST(PortManager, getStatusHandlesNullptr) +{ + PortManager manager{{{}}}; + ASSERT_EQ(manager.getPorts().size(), 1); + + const auto& port_ptr = manager.get(""); + ASSERT_EQ(port_ptr, nullptr); + + ASSERT_NO_FATAL_FAILURE(std::ignore = manager.getStatus()); + + const auto statuses = manager.getStatus(); + ASSERT_EQ(statuses.size(), 0); +} + diff --git a/core/test/test_producer_consumer_port.cpp b/core/test/test_producer_consumer_port.cpp new file mode 100644 index 0000000..c751bb6 --- /dev/null +++ b/core/test/test_producer_consumer_port.cpp @@ -0,0 +1,499 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/buffered_consumer_port.h" +#include "superflow/policy.h" +#include "superflow/producer_port.h" + +#include "gtest/gtest.h" + +using namespace flow; +constexpr auto Single = ConnectPolicy::Single; + +TEST(ProducerConsumer, connectNoThrow) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_NO_THROW(producer->connect(consumer)); +} + +TEST(Producer, disconnectNoThrow) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_NO_THROW(producer->disconnect()); + ASSERT_NO_THROW(producer->disconnect(consumer)); +} + +TEST(Consumer, disconnectNoThrow) +{ + using Consumer = BufferedConsumerPort; + + auto consumer = std::make_shared(); + + ASSERT_NO_THROW(consumer->disconnect()); +} + +TEST(ProducerConsumer, connectMismatchThrows) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_THROW(producer->connect(consumer), std::invalid_argument); +} + +TEST(ProducerConsumer, connectWorksBothWays) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + { + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_NO_THROW(producer->connect(consumer)); + + ASSERT_EQ(producer->numConnections(), 1); + ASSERT_TRUE(consumer->isConnected()); + } + + { + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_NO_THROW(consumer->connect(producer)); + + ASSERT_EQ(producer->numConnections(), 1); + ASSERT_TRUE(consumer->isConnected()); + } +} + +TEST(ProducerConsumer, disconnectWorksBothWays) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + { + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_NO_THROW(producer->connect(consumer)); + + ASSERT_EQ(producer->numConnections(), 1); + ASSERT_TRUE(consumer->isConnected()); + + producer->disconnect(); + + ASSERT_EQ(producer->numConnections(), 0); + ASSERT_FALSE(consumer->isConnected()); + } + + { + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_NO_THROW(consumer->connect(producer)); + + ASSERT_EQ(producer->numConnections(), 1); + ASSERT_TRUE(consumer->isConnected()); + + consumer->disconnect(); + + ASSERT_EQ(producer->numConnections(), 0); + ASSERT_FALSE(consumer->isConnected()); + } +} + +TEST(ProducerConsumer, newConnectToProducerAddsConnection) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer1 = std::make_shared(); + auto consumer2 = std::make_shared(); + + producer->connect(consumer1); + + ASSERT_EQ(producer->numConnections(), 1); + ASSERT_TRUE(consumer1->isConnected()); + + producer->connect(consumer2); + + ASSERT_EQ(producer->numConnections(), 2); + ASSERT_TRUE(consumer1->isConnected()); + ASSERT_TRUE(consumer2->isConnected()); +} + +TEST(ProducerConsumer, newConnectToConsumerThrows) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto consumer = std::make_shared(); + auto producer1 = std::make_shared(); + auto producer2 = std::make_shared(); + + consumer->connect(producer1); + + ASSERT_TRUE(consumer->isConnected()); + ASSERT_EQ(producer1->numConnections(), 1); + ASSERT_EQ(producer2->numConnections(), 0); + + ASSERT_THROW(consumer->connect(producer2), std::invalid_argument); +} + +TEST(ProducerConsumer, multipleConnectOfSamePortNoThrow) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_NO_FATAL_FAILURE(producer->connect(consumer)); + ASSERT_NO_FATAL_FAILURE(producer->connect(consumer)); + ASSERT_NO_FATAL_FAILURE(consumer->connect(producer)); + + ASSERT_EQ(producer->numConnections(), 1); + ASSERT_TRUE(consumer->isConnected()); +} + +TEST(ProducerConsumer, pointersAreFreedOnDisconnect) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + { + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_EQ(producer.use_count(), 1); + ASSERT_EQ(consumer.use_count(), 1); + + producer->connect(consumer); + + ASSERT_EQ(producer.use_count(), 2); + ASSERT_EQ(consumer.use_count(), 3); + + producer->disconnect(); + + ASSERT_EQ(producer.use_count(), 1); + ASSERT_EQ(consumer.use_count(), 1); + } + + { + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_EQ(producer.use_count(), 1); + ASSERT_EQ(consumer.use_count(), 1); + + consumer->connect(producer); + + ASSERT_EQ(producer.use_count(), 2); + ASSERT_EQ(consumer.use_count(), 3); + + consumer->disconnect(); + + ASSERT_EQ(producer.use_count(), 1); + ASSERT_EQ(consumer.use_count(), 1); + } +} + +TEST(ProducerConsumer, numTransactions) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer = std::make_shared(); + + ASSERT_NO_THROW(producer->connect(consumer)); + + EXPECT_EQ(0, producer->getStatus().num_transactions); + EXPECT_EQ(0, consumer->getStatus().num_transactions); + + ASSERT_NO_THROW(producer->send(42)); + + EXPECT_EQ(1, producer->getStatus().num_transactions); + EXPECT_EQ(0, consumer->getStatus().num_transactions); + + ASSERT_NO_FATAL_FAILURE( std::ignore = consumer->getNext().value()); + EXPECT_EQ(1, consumer->getStatus().num_transactions); +} + +TEST(Producer, specificDisconnect) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + auto consumer1 = std::make_shared(); + auto consumer2 = std::make_shared(); + + producer->connect(consumer1); + producer->connect(consumer2); + + ASSERT_EQ(producer->numConnections(), 2); + ASSERT_TRUE(consumer1->isConnected()); + ASSERT_TRUE(consumer2->isConnected()); + + producer->disconnect(consumer1); + + ASSERT_EQ(producer->numConnections(), 1); + ASSERT_FALSE(consumer1->isConnected()); + ASSERT_TRUE(consumer2->isConnected()); + + producer->disconnect(consumer2); + ASSERT_EQ(producer->numConnections(), 0); + ASSERT_FALSE(consumer1->isConnected()); + ASSERT_FALSE(consumer2->isConnected()); +} + +TEST(Producer, generalDisconnect) +{ + using Producer = ProducerPort; + using Consumer = BufferedConsumerPort; + + auto producer = std::make_shared(); + + constexpr size_t num_consumers = 10; + std::vector> consumers; + + for (size_t i = 0; i < num_consumers; ++i) + { + auto consumer = std::make_shared(); + producer->connect(consumer); + + ASSERT_TRUE(consumer->isConnected()); + + consumers.push_back(std::move(consumer)); + } + + ASSERT_EQ(producer->numConnections(), num_consumers); + + producer->disconnect(); + + ASSERT_EQ(producer->numConnections(), 0); + + for (const auto& consumer : consumers) + { + ASSERT_FALSE(consumer->isConnected()); + } +} + +TEST(Producer, conversion) +{ + using Producer = ProducerPort; + + const auto producer = std::make_shared(); + const auto bool_consumer = std::make_shared>(1); + const auto int_consumer = std::make_shared>(1); + + ASSERT_NO_THROW(bool_consumer->connect(producer)); + ASSERT_NO_THROW(int_consumer->connect(producer)); + + producer->send(2); + ASSERT_TRUE(bool_consumer->getNext().value_or(false)); + ASSERT_EQ(int_consumer->getNext().value_or(-1), 2); +} + +namespace +{ +struct IntClass +{ + std::string name; + int val; + + operator int() const // NOLINT intentionally implicit + { + return val; + } + + explicit operator bool() const + { + return val; + } +}; +} + +TEST(Producer, implicit_conversion) +{ + using Producer = ProducerPort; + + const auto producer = std::make_shared(); + const auto consumer = std::make_shared>(1); + const auto int_consumer = std::make_shared>(1); + + ASSERT_NO_THROW(consumer->connect(producer)); + ASSERT_NO_THROW(int_consumer->connect(producer)); + + producer->send({"hei", 2}); + ASSERT_EQ(int_consumer->getNext().value_or(-1), 2); + ASSERT_EQ(consumer->getNext().value(), 2); +} + +TEST(Producer, explicit_conversion) +{ + using Producer = ProducerPort; + + const auto producer = std::make_shared(); + const auto consumer = std::make_shared>(1); + const auto bool_consumer = std::make_shared>(1); + + ASSERT_NO_THROW(consumer->connect(producer)); + ASSERT_NO_THROW(bool_consumer->connect(producer)); + + producer->send({"hei", 10}); + ASSERT_EQ(consumer->getNext().value().val, 10); + ASSERT_EQ(bool_consumer->getNext().value_or(false), true); +} + +namespace +{ +struct CIntClass +{ + CIntClass( + const std::string& name_in, + const int val_in + ) + : name{name_in} + , val{val_in} + {} + + CIntClass( + const std::string& name_in + ) + : name{name_in} + , val{0} + {} + + explicit CIntClass( + const IntClass& int_class + ) + : name{int_class.name} + , val{int_class.val} + {} + + std::string name; + int val; +}; +} + +TEST(Producer, implicit_ctor_conversion) +{ + const auto cint_producer = std::make_shared>(); + const auto string_producer = std::make_shared>(); + const auto consumer = std::make_shared>(1); + + ASSERT_NO_THROW(consumer->connect(cint_producer)); + ASSERT_NO_THROW(string_producer->connect(consumer)); + + { + const CIntClass v = {"hei", 2}; + cint_producer->send(v); + + const auto res = consumer->getNext().value(); + ASSERT_EQ(res.name, v.name); + ASSERT_EQ(res.val, v.val); + } + + { + const std::string v = "hei"; + string_producer->send(v); + + const auto res = consumer->getNext().value(); + ASSERT_EQ(res.name, v); + ASSERT_EQ(res.val, 0); + } +} + +TEST(Producer, explicit_ctor_conversion) +{ + const auto cint_producer = std::make_shared>(); + const auto int_producer = std::make_shared>(); + const auto consumer = std::make_shared>(1); + + ASSERT_NO_THROW(consumer->connect(int_producer)); + ASSERT_NO_THROW(cint_producer->connect(consumer)); + + { + const CIntClass v = {"hei", 2}; + cint_producer->send(v); + + const auto res = consumer->getNext().value(); + ASSERT_EQ(res.name, v.name); + ASSERT_EQ(res.val, v.val); + } + + { + const IntClass v = {"heia", 3}; + int_producer->send(v); + + const auto res = consumer->getNext().value(); + ASSERT_EQ(res.name, v.name); + ASSERT_EQ(res.val, v.val); + } +} + +namespace +{ +class Base +{ +public: + virtual ~Base() = default; +}; + +class Derived : public Base +{}; +} + +TEST(Producer, upCast) +{ + using Producer = ProducerPort; + + const auto producer = std::make_shared(); + const auto base_consumer = std::make_shared>(1); + const auto derived_consumer = std::make_shared>(1); + + ASSERT_NO_THROW(base_consumer->connect(producer)); + ASSERT_NO_THROW(derived_consumer->connect(producer)); + + producer->send(Derived{}); + ASSERT_TRUE(base_consumer->hasNext()); + ASSERT_TRUE(derived_consumer->hasNext()); +} + +struct Ax { size_t a; }; +struct Bx : public Ax { float f; }; +struct Cx { std::string str; }; + +TEST(Producer, incompatible_types) +{ + using Producer = ProducerPort; + using Consumer_B = BufferedConsumerPort; + using Consumer_C = BufferedConsumerPort; + + const auto producer = std::make_shared(); + const auto b_consum = std::make_shared(1); + const auto c_consum = std::make_shared(1); + + ASSERT_NO_THROW(producer->connect(b_consum)); + ASSERT_THROW(producer->connect(c_consum), std::invalid_argument); + + const Bx bx{{42}, 2.f}; + producer->send(bx); + + ASSERT_TRUE(b_consum->hasNext()); + ASSERT_EQ(b_consum->getNext().value().a, 42); +} diff --git a/core/test/test_proxel_status.cpp b/core/test/test_proxel_status.cpp new file mode 100644 index 0000000..40445da --- /dev/null +++ b/core/test/test_proxel_status.cpp @@ -0,0 +1,88 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/proxel.h" + +#include "gtest/gtest.h" + +using namespace flow; + +class MyProxel : public Proxel +{ +public: + MyProxel() = default; + + explicit MyProxel(const State state) + { + setState(state); + }; + + explicit MyProxel(const std::string& status_info) + { + setStatusInfo(status_info); + }; + + ~MyProxel() override = default; + + void start() override {}; + + void stop() noexcept override {}; + + void pushState(const State state) + { + setState(state); + } + + void pushStatusInfo(const std::string& status_info) + { + setStatusInfo(status_info); + } +}; + +TEST(ProxelStatus, default_state_is_undefined) +{ + MyProxel proxel; + ASSERT_EQ(proxel.getStatus().state, ProxelStatus::State::Undefined); +} + +TEST(ProxelStatus, setState_works) +{ + MyProxel proxel; + + proxel.pushState(ProxelStatus::State::Running); + ASSERT_EQ(proxel.getStatus().state, ProxelStatus::State::Running); +} + +TEST(ProxelStatus, setState_works_from_ctor) +{ + MyProxel proxel{ProxelStatus::State::Paused}; + ASSERT_EQ(proxel.getStatus().state, ProxelStatus::State::Paused); + + proxel.pushState(ProxelStatus::State::Running); + ASSERT_EQ(proxel.getStatus().state, ProxelStatus::State::Running); +} + +TEST(ProxelStatus, default_status_info_is_empty) +{ + MyProxel proxel; + ASSERT_EQ(proxel.getStatus().info, ""); +} + +TEST(ProxelStatus, setStatusInfo_works) +{ + MyProxel proxel; + + const std::string info = "hallo"; + proxel.pushStatusInfo(info); + ASSERT_EQ(proxel.getStatus().info, info); +} + +TEST(ProxelStatus, setStatusInfo_works_from_ctor) +{ + const std::string orig_info = "hallo"; + MyProxel proxel{orig_info}; + ASSERT_EQ(proxel.getStatus().info, orig_info); + + proxel.pushState(ProxelStatus::State::Running); + const std::string other_info = "heihei"; + proxel.pushStatusInfo(other_info); + ASSERT_EQ(proxel.getStatus().info, other_info); +} diff --git a/core/test/test_proxel_timer.cpp b/core/test/test_proxel_timer.cpp new file mode 100644 index 0000000..92d45a1 --- /dev/null +++ b/core/test/test_proxel_timer.cpp @@ -0,0 +1,107 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/utils/proxel_timer.h" + +#include "gtest/gtest.h" + +#include +#include + +using namespace flow; + +TEST(ProxelTimer, stop_before_start_throws) +{ + ProxelTimer timer; + ASSERT_THROW(timer.stop(), std::runtime_error); +} + +TEST(ProxelTimer, start_before_stop_doesnt_throw) +{ + ProxelTimer timer; + timer.start(); + ASSERT_NO_THROW(timer.stop()); +} + +TEST(ProxelTimer, peek) +{ + ProxelTimer timer; + timer.start(); + double elapsed_time; + ASSERT_NO_THROW(elapsed_time = timer.peek()); + ASSERT_GT(elapsed_time, 0.); +} + +TEST(ProxelTimer, get_run_count) +{ + using namespace std::chrono_literals; + { + ProxelTimer timer; + timer.start(); + timer.stop(); + timer.start(); + timer.stop(); + + EXPECT_EQ(2, timer.getRunCount()); + } +} + +TEST(ProxelTimer, get_busyness) +{ + using namespace std::chrono_literals; + { + ProxelTimer timer; + timer.start(); + timer.stop(); + + EXPECT_EQ(1, timer.getAverageBusyness()); + } + + { + ProxelTimer timer; + + timer.start(); + std::this_thread::sleep_for(100us); + timer.stop(); + + std::this_thread::sleep_for(10us); + + timer.start(); + std::this_thread::sleep_for(100us); + timer.stop(); + + EXPECT_LE(0.1, timer.getAverageBusyness()); + } +} + +TEST(ProxelTimer, get_average_processing_time) +{ + using namespace std::chrono_literals; + + { + ProxelTimer timer; + timer.start(); + std::this_thread::sleep_for(1ms); + const auto total = timer.stop(); + const auto average = timer.getAverageProcessingTime(); + + EXPECT_EQ(total, average); + } + { + ProxelTimer timer; + timer.start(); + std::this_thread::sleep_for(1ms); + auto total = timer.stop(); + timer.start(); + total += timer.stop(); + + const auto average = timer.getAverageProcessingTime(); + EXPECT_EQ(total/timer.getRunCount(), average); + } +} + +TEST(ProxelTimer, get_status_info) +{ + ProxelTimer timer; + std::string info; + EXPECT_NO_FATAL_FAILURE(info = timer.getStatusInfo()); + EXPECT_EQ(info.back(), '0'); +} diff --git a/core/test/test_requester_responder_port.cpp b/core/test/test_requester_responder_port.cpp new file mode 100644 index 0000000..5a0bd47 --- /dev/null +++ b/core/test/test_requester_responder_port.cpp @@ -0,0 +1,548 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/requester_port.h" + +#include "gtest/gtest.h" + +using namespace flow; + +TEST(RequesterResponderPort, matchingPortsConnects) +{ + auto out = std::make_shared>(); + auto in1 = std::make_shared>([](int){}); + auto in2 = std::make_shared>([](int){}); + + ASSERT_NO_FATAL_FAILURE(out->connect(in1)); +} + +TEST(RequesterPort, disconnectNoThrow) +{ + auto out = std::make_shared>(); + + ASSERT_NO_FATAL_FAILURE(out->disconnect()); +} + +TEST(ResponderPort, disconnectNoThrow) +{ + auto in = std::make_shared>([](int) + {}); + + ASSERT_NO_FATAL_FAILURE(in->disconnect()); +} + +TEST(RequesterResponderPort, connectAfterDisconnectDoesNotThrow) +{ + auto out = std::make_shared>(); + auto in1 = std::make_shared>([](int){}); + + ASSERT_NO_FATAL_FAILURE(out->connect(in1)); + ASSERT_NO_FATAL_FAILURE(out->disconnect()); + + auto in2 = std::make_shared>([](int){}); + ASSERT_NO_FATAL_FAILURE(in2->connect(out)); +} + +TEST(RequesterResponderPort, mismatchingPortsThrows) +{ + auto master = std::make_shared>(); + auto slave1 = std::make_shared>([](float) + {}); + auto slave2 = std::make_shared>([](int) -> bool + { return false; }); + + ASSERT_THROW(master->connect(slave1), std::invalid_argument); + ASSERT_THROW(slave1->connect(master), std::invalid_argument); + ASSERT_THROW(slave2->connect(master), std::invalid_argument); +} + +TEST(RequesterResponderPort, connectWorksBothWays) +{ + { + auto out = std::make_shared>(); + auto in = std::make_shared>([](int) + {}); + + ASSERT_FALSE(out->isConnected()); + ASSERT_FALSE(in->isConnected()); + + out->connect(in); + + ASSERT_TRUE(out->isConnected()); + ASSERT_TRUE(in->isConnected()); + } + + { + auto out = std::make_shared>(); + auto in = std::make_shared>([](int) + {}); + + ASSERT_FALSE(out->isConnected()); + ASSERT_FALSE(in->isConnected()); + + in->connect(out); + + ASSERT_TRUE(out->isConnected()); + ASSERT_TRUE(in->isConnected()); + } +} + +TEST(RequesterResponderPort, newConnectToMasterThrows) +{ + auto requester = std::make_shared>(); + + auto responder_1 = std::make_shared>([](int){}); + auto responder_2 = std::make_shared>([](int){}); + + requester->connect(responder_1); + + ASSERT_TRUE(requester->isConnected()); + ASSERT_TRUE(responder_1->isConnected()); + + ASSERT_THROW(requester->connect(responder_2), std::runtime_error); + + ASSERT_TRUE(requester->isConnected()); + ASSERT_TRUE(responder_1->isConnected()); + ASSERT_FALSE(responder_2->isConnected()); +} + +TEST(RequesterResponderPort, newConnectToSlaveDoesNotDisconnectOldMaster) +{ + auto out1 = std::make_shared>(); + auto out2 = std::make_shared>(); + auto in = std::make_shared>([](int){}); + + in->connect(out1); + + ASSERT_TRUE(in->isConnected()); + ASSERT_EQ(in->getStatus().num_connections, 1); + ASSERT_TRUE(out1->isConnected()); + + in->connect(out2); + + ASSERT_TRUE(in->isConnected()); + ASSERT_EQ(in->getStatus().num_connections, 2); + ASSERT_TRUE(out1->isConnected()); + ASSERT_TRUE(out2->isConnected()); +} + +TEST(RequesterResponderPort, doubleConnectOfSamePortsNoFail) +{ + auto requester = std::make_shared>(); + auto responder_1 = std::make_shared>([](int){}); + auto responder_2 = std::make_shared>([](int){}); + + ASSERT_NO_FATAL_FAILURE(requester->connect(responder_1)); + ASSERT_NO_FATAL_FAILURE(requester->connect(responder_1)); + ASSERT_NO_FATAL_FAILURE(responder_1->connect(requester)); +} + +TEST(RequesterResponderPort, disconnectWorksBothWays) +{ + { + auto out = std::make_shared>(); + auto in = std::make_shared>([](int){}); + + out->connect(in); + + ASSERT_TRUE(out->isConnected()); + ASSERT_TRUE(in->isConnected()); + + out->disconnect(); + + ASSERT_FALSE(out->isConnected()); + ASSERT_FALSE(in->isConnected()); + } + + { + auto out = std::make_shared>(); + auto in = std::make_shared>([](int) + {}); + + out->connect(in); + + ASSERT_TRUE(out->isConnected()); + ASSERT_TRUE(in->isConnected()); + + in->disconnect(); + + ASSERT_FALSE(out->isConnected()); + ASSERT_FALSE(in->isConnected()); + } +} + +TEST(RequesterResponderPort, slaveRespondsToMaster) +{ + auto master = std::make_shared>(); + auto slave = std::make_shared>([](int a) + { return a; }); + + ASSERT_NO_FATAL_FAILURE(master->connect(slave)); + auto value = master->request(42); + ASSERT_EQ(value, 42); +} + +TEST(RequesterResponderPort, requestAfterDisconnectThrows) +{ + auto master = std::make_shared>(); + auto slave = std::make_shared>([](int a) + { return a; }); + + ASSERT_THROW(master->request(42), std::runtime_error); +} + +TEST(RequesterResponderPort, SlaveCanModifyValuePassedByReference) +{ + auto master = std::make_shared>(); + auto slave = std::make_shared>([](int& a, int b) + { + a += 2; + return a + b; + }); + + ASSERT_NO_FATAL_FAILURE(master->connect(slave)); + int a = 20; + auto value = master->request(a, 20); + ASSERT_EQ(value, 42); + ASSERT_EQ(a, 22); +} + +TEST(RequesterResponderPort, PassingSharedPointers) +{ + class A + { + public: + int value{0}; + }; + + ResponderPort)> slave([](std::shared_ptr ptr) + { ptr->value = 42; }); + + auto a = std::make_shared(); + ASSERT_EQ(a->value, 0); + slave.respond(a); + ASSERT_EQ(a->value, 42); +} + +TEST(RequesterResponderPort, BindMemberFunction) +{ + class A + { + int func() + { return value; } + + public: + int value{42}; + + using PortType = ResponderPort; + std::shared_ptr port = std::make_shared([this]() + { return func(); }); + }; + + A a; + a.value = 40; + auto value = a.port->respond(); + ASSERT_EQ(value, 40); +} + +TEST(RequesterResponderPort, pointersAreFreedOnDisconnect) +{ + { + auto out = std::make_shared>(); + auto in = std::make_shared>([](int) + {}); + + ASSERT_EQ(out.use_count(), 1); + ASSERT_EQ(in.use_count(), 1); + + out->connect(in); + + ASSERT_EQ(out.use_count(), 2); + ASSERT_EQ(in.use_count(), 3); + + out->disconnect(); + + ASSERT_EQ(out.use_count(), 1); + ASSERT_EQ(in.use_count(), 1); + } + + { + auto out = std::make_shared>(); + auto in = std::make_shared>([](int) + {}); + + ASSERT_EQ(out.use_count(), 1); + ASSERT_EQ(in.use_count(), 1); + + in->connect(out); + + ASSERT_EQ(out.use_count(), 2); + ASSERT_EQ(in.use_count(), 3); + + in->disconnect(); + + ASSERT_EQ(out.use_count(), 1); + ASSERT_EQ(in.use_count(), 1); + } +} + +TEST(RequesterResponderPort, conversion) +{ + { + const auto responder = std::make_shared>( + [](){ return 10; } + ); + const auto int_requester = std::make_shared>(); + + ASSERT_NO_THROW(int_requester->connect(responder)); + ASSERT_EQ(int_requester->request(), 10); + } + + { + const auto responder = std::make_shared>( + [](){ return 10; } + ); + const auto bool_requester = std::make_shared>(); + + ASSERT_NO_THROW(bool_requester->connect(responder)); + ASSERT_EQ(bool_requester->request(), true); + } +} + +namespace +{ +struct IntClass +{ + std::string name; + int val; + + operator int() const // NOLINT intentionally implicit + { + return val; + } + + explicit operator bool() const + { + return val; + } +}; +} + +TEST(RequesterResponderPort, implicit_conversion) +{ + { + const auto responder = std::make_shared>( + [](){ return IntClass{"name", 10}; } + ); + const auto int_class_requester = std::make_shared>(); + + ASSERT_NO_THROW(int_class_requester->connect(responder)); + const IntClass int_class = int_class_requester->request(); + ASSERT_EQ(int_class.name, "name"); + ASSERT_EQ(int_class.val, 10); + } + + { + const auto responder = std::make_shared>( + [](){ return IntClass{"name", 10}; } + ); + const auto int_requester = std::make_shared>(); + + ASSERT_NO_THROW(int_requester->connect(responder)); + ASSERT_EQ(int_requester->request(), 10); + } +} + +TEST(RequesterResponderPort, explicit_conversion) +{ + { + const auto responder = std::make_shared>( + [](){ return IntClass{"name", 10}; } + ); + const auto requester = std::make_shared>(); + + ASSERT_NO_THROW(requester->connect(responder)); + const IntClass int_class = requester->request(); + ASSERT_EQ(int_class.name, "name"); + ASSERT_EQ(int_class.val, 10); + } + + { + const auto responder = std::make_shared>( + [](){ return IntClass{"name", 10}; } + ); + const auto requester = std::make_shared>(); + + ASSERT_NO_THROW(requester->connect(responder)); + ASSERT_EQ(requester->request(), true); + } +} + +namespace +{ +struct CIntClass +{ + CIntClass( + const std::string& name_in, + const int val_in + ) + : name{name_in} + , val{val_in} + {} + + CIntClass( + const std::string& name_in + ) + : name{name_in} + , val{0} + {} + + explicit CIntClass( + const IntClass& int_class + ) + : name{int_class.name} + , val{int_class.val} + {} + + std::string name; + int val; +}; +} + +TEST(RequesterResponderPort, implicit_ctor_conversion) +{ + { + const auto responder = std::make_shared>( + [](){ return CIntClass{"namea", 10}; } + ); + const auto requester = std::make_shared>(); + + ASSERT_NO_THROW(requester->connect(responder)); + + const auto res = requester->request(); + ASSERT_EQ(res.name, "namea"); + ASSERT_EQ(res.val, 10); + } + + { + const auto responder = std::make_shared>( + [](){ return "name"; } + ); + const auto requester = std::make_shared>(); + + ASSERT_NO_THROW(requester->connect(responder)); + + const auto res = requester->request(); + ASSERT_EQ(res.name, "name"); + ASSERT_EQ(res.val, 0); + } +} + +TEST(RequesterResponderPort, explicit_ctor_conversion) +{ + { + const auto responder = std::make_shared>( + [](){ return CIntClass{"namea", 10}; } + ); + const auto requester = std::make_shared>(); + + ASSERT_NO_THROW(requester->connect(responder)); + + const CIntClass res = requester->request(); + ASSERT_EQ(res.name, "namea"); + ASSERT_EQ(res.val, 10); + } + + { + const auto responder = std::make_shared>( + [](){ return IntClass{"name", 15}; } + ); + const auto requester = std::make_shared>(); + + ASSERT_NO_THROW(requester->connect(responder)); + + const CIntClass res = requester->request(); + ASSERT_EQ(res.name, "name"); + ASSERT_EQ(res.val, 15); + } +} + +namespace +{ +class Base +{ +public: + virtual ~Base() = default; +}; + +class Derived : public Base +{}; +} + +TEST(RequesterResponderPort, up_cast) +{ + { + const auto responder = std::make_shared>( + [](){ return Derived{}; } + ); + const auto requester = std::make_shared>(); + + ASSERT_NO_THROW(responder->connect(requester)); + + const Derived res = requester->request(); + } + + { + const auto responder = std::make_shared>( + [](){ return Derived{}; } + ); + const auto requester = std::make_shared>(); + + ASSERT_NO_THROW(responder->connect(requester)); + + const Base res = requester->request(); + } + + { + const auto responder = std::make_shared>( + [](){ return Derived{}; } + ); + const auto requester = std::make_shared>(); + + ASSERT_NO_THROW(responder->connect(requester)); + + const Derived res = requester->request(); + } + + { + const auto responder = std::make_shared>( + [](){ return Derived{}; } + ); + const auto requester = std::make_shared>(); + + ASSERT_NO_THROW(responder->connect(requester)); + + const Base res = requester->request(); + } +} + +TEST(RequesterResponderPort, std_variant_conversions) +{ + { + const auto responder = std::make_shared>( + [](){ return 10; } + ); + const auto requester = std::make_shared()>>(); + + ASSERT_NO_THROW(responder->connect(requester)); + ASSERT_EQ(std::get(requester->request()), 10); + } + + { + const auto responder = std::make_shared>( + [](){ return false; } + ); + const auto requester = std::make_shared()>>(); + + ASSERT_NO_THROW(responder->connect(requester)); + ASSERT_EQ(std::get(requester->request()), false); + } +} diff --git a/core/test/test_shared_mutexed.cpp b/core/test/test_shared_mutexed.cpp new file mode 100644 index 0000000..1872b04 --- /dev/null +++ b/core/test/test_shared_mutexed.cpp @@ -0,0 +1,139 @@ +#include "superflow/utils/shared_mutexed.h" +#include "gtest/gtest.h" + +#include + +using namespace std::chrono_literals; + +TEST(SharedMutexed, multiple_read_can_happen_simultaneously) +{ + const flow::SharedMutexed mutexed{"original"}; + + std::promise release_readers; + auto must_wait = release_readers.get_future().share(); + std::promise all_readers_started; + std::atomic_size_t active_readers{0}; + std::atomic_size_t simultaneous_readers{0}; + + std::vector> readers; + + constexpr size_t num_readers{10}; + for (size_t i{0}; i < num_readers; ++i) + { + readers.emplace_back( + std::async( + std::launch::async, + [&all_readers_started, &must_wait, &active_readers, &simultaneous_readers, &mutexed] + { + return mutexed.read( + [&all_readers_started, &must_wait, &active_readers,&simultaneous_readers](const auto& value)-> std::string + { + ++simultaneous_readers; + + if (++active_readers == num_readers) + { all_readers_started.set_value(); } + // All reader threads have certainly been created + + must_wait.wait(); + --simultaneous_readers; + --active_readers; + return value; + } + ); + } + ) + ); + } + + { // Assert that all readers have certainly been created + const auto future_status = all_readers_started.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + + EXPECT_EQ(active_readers, num_readers); + EXPECT_EQ(simultaneous_readers.load(), num_readers); + } + + release_readers.set_value(); + for (auto& reader: readers) + { ASSERT_EQ("original", reader.get()); } + + EXPECT_EQ(active_readers, 0); +} + +TEST(SharedMutexed, store_must_wait_for_multiple_read) +{ + flow::SharedMutexed mutexed{"original"}; + + std::promise release_readers; + auto must_wait = release_readers.get_future().share(); + std::atomic_size_t active_readers{0}; + + std::vector> readers; + constexpr size_t num_readers{10}; + { + std::promise all_readers_started; + for (size_t i{0}; i < num_readers; ++i) + { + readers.emplace_back( + std::async( + std::launch::async, [&all_readers_started, &must_wait, &active_readers, &mutexed] + { + return mutexed.read( + [&all_readers_started, &must_wait, &active_readers](const auto& value) -> std::string + { + if (++active_readers == num_readers) + { all_readers_started.set_value(); } + // All reader threads have certainly been created + + must_wait.wait(); + --active_readers; + return value; + } + ); + } + ) + ); + } + + { // Assert that all readers have certainly been created + const auto future_status = all_readers_started.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + + // They are all busy with 'read', but are blocked by the 'must_wait' + ASSERT_EQ(active_readers, num_readers); + } + } + + std::promise writer_started; + auto writer = std::async( + std::launch::async, [&writer_started, &active_readers, &mutexed] + { + writer_started.set_value(); + mutexed.store("store"); + mutexed.store(mutexed.load() + "," + std::to_string(active_readers)); + } + ); + + { // Assert that the writer has certainly been created + const auto future_status = writer_started.get_future().wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + } + + // When writer is eventually started, + // all readers are still blocked in 'read', so writer is also blocked. + ASSERT_EQ(active_readers, num_readers); + + // All the readers must be served before writer can lock mutexed. + // All readers will read the initial value + release_readers.set_value(); + for (auto& reader: readers) + { + ASSERT_EQ("original", reader.get()); + } + + const auto future_status = writer.wait_for(1s); + ASSERT_NE(future_status, std::future_status::timeout); + + EXPECT_EQ(active_readers, 0); + EXPECT_EQ("store,0", mutexed); +} diff --git a/core/test/test_signal_waiter.cpp b/core/test/test_signal_waiter.cpp new file mode 100644 index 0000000..5280f92 --- /dev/null +++ b/core/test/test_signal_waiter.cpp @@ -0,0 +1,114 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/utils/signal_waiter.h" + +#include "gtest/gtest.h" + +#include +#include + +using namespace flow; + +TEST(SignalWaiter, waits) +{ + SignalWaiter waiter; + + ASSERT_FALSE(waiter.hasGottenSignal()); + + std::raise(SIGINT); + + ASSERT_TRUE(waiter.hasGottenSignal()); +} + +TEST(SignalWaiter, stopsWaitingOnDTOR) +{ + std::shared_future waiter_future; + { + SignalWaiter waiter; + ASSERT_FALSE(waiter.hasGottenSignal()); + waiter_future = waiter.getFuture(); + } + waiter_future.get(); +} + +namespace +{ +constexpr size_t num_workers = 50; + +auto createSignalWaiterWorkers(int signal) +{ + std::vector> workers; + std::vector workers_done(num_workers, false); + std::vector> workers_has_started(num_workers); + std::vector> workers_has_ended(num_workers); + + for (size_t i = 0; i < num_workers; ++i) + { + workers.push_back( + std::async( + std::launch::async, + [&worker_done = workers_done[i], + &worker_has_started = workers_has_started[i], + &worker_has_ended = workers_has_ended[i], + signal]() + { + SignalWaiter waiter{{signal}}; + worker_has_started.set_value(); + waiter.getFuture().wait(); + + worker_done = true; + worker_has_ended.set_value(); + } + ) + ); + } + + return std::make_tuple(std::move(workers), + std::move(workers_done), + std::move(workers_has_started), + std::move(workers_has_ended)); +} + +bool all(const std::vector& values) +{ + return std::all_of( + values.begin(), + values.end(), + [](uint8_t status) + { return status; } + ); +} + +void waitFor(std::vector>& promises_to_wait_for) +{ + for (auto& promise: promises_to_wait_for) + { + promise.get_future().wait(); + } +} +} + +TEST(SignalWaiter, multiThreaded) +{ + auto [sigint_workers, + sigint_workers_done, + sigint_workers_has_started, + sigint_workers_has_ended] = createSignalWaiterWorkers(SIGINT); + + auto [sigterm_workers, + sigterm_workers_done, + sigterm_workers_has_started, + sigterm_workers_has_ended] = createSignalWaiterWorkers(SIGTERM); + + waitFor(sigint_workers_has_started); + ASSERT_FALSE(all(sigint_workers_done)); + std::raise(SIGINT); + waitFor(sigint_workers_has_ended); + ASSERT_TRUE(all(sigint_workers_done)); + + waitFor(sigterm_workers_has_started); + ASSERT_FALSE(all(sigterm_workers_done)); + std::raise(SIGTERM); + waitFor(sigterm_workers_has_ended); + ASSERT_TRUE(all(sigterm_workers_done)); +} + diff --git a/core/test/test_sleeper.cpp b/core/test/test_sleeper.cpp new file mode 100644 index 0000000..8c9cedf --- /dev/null +++ b/core/test/test_sleeper.cpp @@ -0,0 +1,32 @@ +// Copyright (c) 2022, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/utils/sleeper.h" + +#include "gtest/gtest.h" + +#include + +namespace flow +{ +TEST(Sleeper, check) +{ + using namespace std::chrono_literals; + + + Sleeper sleeper(10ms); + const auto begin = Sleeper::Clock::now(); + for (int i =0; i<10; ++i) + { + if (i > 4) + { + sleeper.setSleepPeriod(5ms); + } + sleeper.sleepForRemainderOfPeriod(); + + } + const auto end = Sleeper::Clock::now(); + const auto expected_duration = 5*(10ms + 5ms); + const auto actual_duration = std::chrono::duration_cast(end - begin); + //std::cout << "expected_duration: " << expected_duration.count() << std::endl; + //std::cout << "actual_duration: " << actual_duration.count() << std::endl; + ASSERT_TRUE(actual_duration == expected_duration);} +} diff --git a/core/test/test_throttle.cpp b/core/test/test_throttle.cpp new file mode 100644 index 0000000..7251a3c --- /dev/null +++ b/core/test/test_throttle.cpp @@ -0,0 +1,127 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/utils/blocker.h" +#include "superflow/utils/throttle.h" + +#include "gtest/gtest.h" + +#include +#include + +using namespace flow; + +TEST(Throttle, throttling) +{ + std::vector result; + Blocker blocker; + + const auto publish_fn = [&result, &blocker](std::string&& s) + { + result.push_back(s); + blocker.release(); + }; + + constexpr auto publish_rate = std::chrono::milliseconds(100); + + { + Throttle th(publish_fn, publish_rate); + + th.push("first"); // Push the first data. + + EXPECT_TRUE(blocker.block()); // Wait to ensure throttle has pushed. + blocker.rearm(); // block can be called again + // As soon at it has published, + // do a double push so that skip1 should be overwritten before next publish + th.push("skip1"); + th.push("second"); + + EXPECT_TRUE(blocker.block()); // Wait to ensure throttle has pushed again + blocker.rearm(); // block can be called again + + th.push("skip2"); + th.push("last"); + + blocker.block(); + ASSERT_EQ(result.back(), "last"); + } + + ASSERT_EQ(result.size(), 3); + ASSERT_EQ(result[0], "first"); + ASSERT_EQ(result[1], "second"); + ASSERT_EQ(result[2], "last"); +} + +inline const void* addr(const std::string& str) +{ return static_cast(str.c_str()); } + + +TEST(Throttle, move_data) +{ + const std::string contents(30, 'x'); + std::string str = contents; + const auto str_address = addr(str); + + ASSERT_NE(addr(contents), str_address); + ASSERT_EQ(contents, str); // Can compare contents even if str is moved. + + std::string result; + bool published = false; + Blocker blocker; + const auto publish_fn = [&result, &published, &blocker](auto&& s) + { + published = true; + result = std::forward(s); + blocker.release(); + }; + constexpr auto publish_rate = std::chrono::microseconds(1); + + Throttle th(publish_fn, publish_rate); + + th.push(std::move(str)); + blocker.block(); + EXPECT_EQ(published, true); + EXPECT_EQ(contents, result); + EXPECT_EQ(str_address, addr(result)); +} + +TEST(Throttle, copy_data) +{ + const std::string str(30, 'x'); + const auto str_address = addr(str); + + std::string result; + bool published = false; + Blocker blocker; + const auto publish_fn = [&result, &published, &blocker](auto&& s) + { + published = true; + result = std::forward(s); + blocker.release(); + }; + constexpr auto publish_rate = std::chrono::microseconds(1); + + Throttle th(publish_fn, publish_rate); + + th.push(str); + blocker.block(); + + ASSERT_EQ(published, true); + EXPECT_EQ(str, result); + EXPECT_NE(str_address, addr(result)); +} + +TEST(Throttle, exception) +{ + Blocker blocker; + const auto publish_fn = [&blocker](int) + { + unblockOnThreadExit(blocker); + throw std::runtime_error("jalla jalla"); + }; + const auto publish_rate = std::chrono::microseconds(1); + + Throttle th(publish_fn, publish_rate); + EXPECT_NO_THROW(th.push(42)); + + blocker.block(); + EXPECT_THROW(th.push(42), std::runtime_error); +} diff --git a/core/test/threaded_proxel.h b/core/test/threaded_proxel.h new file mode 100644 index 0000000..07fd4b5 --- /dev/null +++ b/core/test/threaded_proxel.h @@ -0,0 +1,52 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/proxel.h" + +#include + +namespace flow +{ +class ThreadedProxel : public Proxel +{ +public: + ThreadedProxel() + : thread_id_{} + , start_was_called_{false} + , stop_was_called_{false} + {} + + using Ptr = std::shared_ptr; + + void start() override + { + start_was_called_ = true; + thread_id_ = std::this_thread::get_id(); + } + + void stop() noexcept override + { + stop_was_called_ = true; + } + + std::thread::id getThreadId() const + { + return thread_id_; + } + + bool startWasCalled() const + { + return start_was_called_; + } + + bool stopWasCalled() const + { + return stop_was_called_; + } + +private: + std::thread::id thread_id_; + bool start_was_called_; + bool stop_was_called_; +}; +} diff --git a/curses/CMakeLists.txt b/curses/CMakeLists.txt new file mode 100644 index 0000000..1df6f13 --- /dev/null +++ b/curses/CMakeLists.txt @@ -0,0 +1,17 @@ +project(curses) +init_module() + +find_package(ncursescpp REQUIRED) + +add_library_boilerplate() + +target_link_libraries(${target_name} + PUBLIC + superflow::core + PRIVATE + ncursescpp::ncursescpp + ) + +if (BUILD_TESTS) + add_subdirectory(test) +endif() \ No newline at end of file diff --git a/curses/curses-config.cmake.in b/curses/curses-config.cmake.in new file mode 100644 index 0000000..4e1e063 --- /dev/null +++ b/curses/curses-config.cmake.in @@ -0,0 +1,10 @@ +@PACKAGE_INIT@ +message(STATUS "* Found @CMAKE_PROJECT_NAME@::@PROJECT_NAME@: " "${CMAKE_CURRENT_LIST_FILE}") +include(CMakeFindDependencyMacro) + +find_dependency(ncursescpp) + +set(@CMAKE_PROJECT_NAME@_@PROJECT_NAME@_FOUND TRUE) + +check_required_components(@PROJECT_NAME@) +message(STATUS "* Loading @CMAKE_PROJECT_NAME@::@PROJECT_NAME@ complete") diff --git a/curses/include/superflow/curses/graph_gui.h b/curses/include/superflow/curses/graph_gui.h new file mode 100644 index 0000000..de3aec2 --- /dev/null +++ b/curses/include/superflow/curses/graph_gui.h @@ -0,0 +1,69 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/curses/proxel_window.h" +#include "superflow/graph.h" + +#include +#include +#include + +namespace flow::curses +{ +class GraphGUI +{ +public: + explicit GraphGUI(size_t max_ports_shown = 0); + + void spin( + const Graph& graph, + const std::unordered_set& blacklisted_proxels = {} + ); + + void spinOnce( + const Graph& graph, + const std::unordered_set& blacklisted_proxels = {} + ); + +private: + using ProxelSet = std::map; + using StatusSet = std::map; + using WindowSet = std::map; + + static constexpr int window_h_padding = 2; + static constexpr int window_v_padding = 4; + + ProxelSet last_proxels_; + WindowSet windows_; + + size_t max_ports_shown_; + int width_ = -1; + int height_ = -1; + int minimum_window_width_ = 0; + + [[nodiscard]] bool guiSizeHasChanged() const; + + [[nodiscard]] bool proxelSetHasChanged(const ProxelSet& proxels) const; + + static WindowSet createWindows( + const ProxelSet& proxels, + int width, + int height, + int minimum_window_width, + size_t max_ports_shown + ); + + [[nodiscard]] size_t getMaxNumPorts(const StatusSet& statuses) const; + + static int getMinimumWindowWidth(int max_num_ports); + + static int getWindowWidth(int num_cols, int width, int height); + + static int getNumCols( + int num_proxels, + int width, + int height, + int minimum_window_width + ); +}; +} diff --git a/curses/include/superflow/curses/proxel_window.h b/curses/include/superflow/curses/proxel_window.h new file mode 100644 index 0000000..cf9c69b --- /dev/null +++ b/curses/include/superflow/curses/proxel_window.h @@ -0,0 +1,50 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/curses/window.h" +#include "superflow/proxel_status.h" + +namespace flow::curses +{ +class ProxelWindow +{ +public: + ProxelWindow(); + + ProxelWindow( + int x, + int y, + int width, + size_t max_ports_shown = 0 + ); + + void renderStatus( + const std::string& name, + const ProxelStatus& status + ); + + static int getHeight(); + + static constexpr int port_window_width = 10; + static constexpr int port_window_height = 2; + +private: + static constexpr int inner_height = 6; + + int x_; + int y_; + int width_; + size_t max_ports_shown_; + + Window window_; + std::map port_windows_; + + static std::vector getLines(const std::string& str, size_t max_line_length); + + static std::vector split(const std::string& str, char sep); + + static std::vector split(const std::string& str, size_t chunk_size); + + static nccpp::Color getStateColor(ProxelStatus::State state); +}; +} diff --git a/curses/include/superflow/curses/window.h b/curses/include/superflow/curses/window.h new file mode 100644 index 0000000..bce7568 --- /dev/null +++ b/curses/include/superflow/curses/window.h @@ -0,0 +1,34 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/utils/pimpl_h.h" + +#include +#include + +namespace nccpp +{ struct Color; } + +namespace flow::curses +{ +class Window +{ +public: + Window( + int x, + int y, + int width, + int height + ); + + void render( + const std::string& name, + const std::vector& lines, + const nccpp::Color& color + ); + +private: + class impl; + pimpl m_; +}; +} diff --git a/curses/src/graph_gui.cpp b/curses/src/graph_gui.cpp new file mode 100644 index 0000000..35cc288 --- /dev/null +++ b/curses/src/graph_gui.cpp @@ -0,0 +1,278 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/curses/graph_gui.h" + +#include "superflow/utils/signal_waiter.h" + +#include "ncursescpp/ncursescpp.hpp" + +#include +#include + +namespace flow::curses +{ +namespace +{ +[[nodiscard]] std::map getProxelSet( + const Graph& graph, + const std::map& non_blacklisted_statuses +); + +/// \brief Returns the status for all proxels that are not blacklisted, and _all_ crashed proxels +/// (even if they are blacklisted) +std::map getProxelStatuses( + const Graph& graph, + const std::unordered_set& blacklisted_proxels +); +} + +GraphGUI::GraphGUI(const size_t max_ports_shown) + : max_ports_shown_{max_ports_shown} +{} + +void GraphGUI::spin( + const Graph& graph, + const std::unordered_set& blacklisted_proxels +) +{ + nccpp::ncurses().nodelay(true); + SignalWaiter waiter{{SIGINT, SIGTERM}}; + + while (nccpp::ncurses().getch() != 'q' && !waiter.hasGottenSignal()) + { + spinOnce(graph, blacklisted_proxels); + std::this_thread::sleep_for(std::chrono::milliseconds{100}); + } +} + +void GraphGUI::spinOnce( + const Graph& graph, + const std::unordered_set& blacklisted_proxels +) +{ + const auto statuses = getProxelStatuses(graph, blacklisted_proxels); + const auto proxels = getProxelSet(graph, statuses); + + const auto minimum_window_width = getMinimumWindowWidth(static_cast(getMaxNumPorts(statuses))); + + nccpp::ncurses().cbreak(true); + nccpp::ncurses().echo(false); + curs_set(0); + + if (proxelSetHasChanged(proxels) + || guiSizeHasChanged() + || minimum_window_width != minimum_window_width_) + { + last_proxels_ = proxels; + minimum_window_width_ = minimum_window_width; + nccpp::ncurses().get_maxyx(height_, width_); + + // clear all windows + nccpp::ncurses().clear(); + nccpp::ncurses().start_color(); + nccpp::ncurses().bkgd( + static_cast(nccpp::ncurses().color_to_attr( + { + nccpp::colors::white, + nccpp::colors::black + } + )) + ); + + windows_ = createWindows( + proxels, + width_, + height_, + minimum_window_width_, + max_ports_shown_ + ); + } + + for (const auto& kv : statuses) + { + const std::string& name = kv.first; + const ProxelStatus& status = kv.second; + + windows_[name].renderStatus(name, status); + } + + nccpp::ncurses().move(height_ - 1, width_ - 1); + nccpp::ncurses().refresh(); +} + +bool GraphGUI::guiSizeHasChanged() const +{ + int width; + int height; + + nccpp::ncurses().get_maxyx(height, width); + + return width != width_ || height != height_; +} + +bool GraphGUI::proxelSetHasChanged(const ProxelSet& proxels) const +{ + if (last_proxels_.size() != proxels.size()) + { + return true; + } + + auto old_it = last_proxels_.begin(); + auto new_it = proxels.begin(); + + while (old_it != last_proxels_.end() && new_it != proxels.end()) + { + if (old_it->first != new_it->first + || old_it->second != new_it->second) + { + return true; + } + + ++old_it; + ++new_it; + } + + return false; +} + +GraphGUI::WindowSet GraphGUI::createWindows( + const ProxelSet& proxels, + const int width, + const int height, + const int minimum_window_width, + const size_t max_ports_shown +) +{ + const auto num_proxels = static_cast(proxels.size()); + const int num_cols = getNumCols(num_proxels, width, height, minimum_window_width); + const int num_rows = (num_proxels + num_cols - 1) / num_cols; + const int window_width = getWindowWidth(num_cols, width, height); + const int window_height = ProxelWindow::getHeight(); + + WindowSet windows; + + auto it = proxels.begin(); + + for (int i = 0; i < num_rows; ++i) + { + for (int j = 0; j < num_cols; ++j) + { + if (it == proxels.end()) + { + break; + } + + const int x = (j + 1) * window_h_padding + j * window_width; + const int y = (i + 1) * window_v_padding + i * window_height; + + ProxelWindow window{ + x, + y, + window_width, + max_ports_shown + }; + + const std::string& name = it->first; + + windows[name] = std::move(window); + ++it; + } + } + + return windows; +} + +size_t GraphGUI::getMaxNumPorts(const StatusSet& statuses) const +{ + size_t max_num_ports = 0; + + for (const auto& kv : statuses) + { + max_num_ports = std::max(max_num_ports, kv.second.ports.size()); + } + + if (max_ports_shown_ > 0) + { + max_num_ports = std::min(max_num_ports, max_ports_shown_); + } + + return max_num_ports; +} + +int GraphGUI::getMinimumWindowWidth(const int max_num_ports) +{ + return std::max( + 2 + max_num_ports * ProxelWindow::port_window_width, + 20 + ); +} + +int GraphGUI::getWindowWidth( + const int num_cols, + const int width, + const int) +{ + return (width - (num_cols + 1) * window_h_padding) / num_cols; +} + +int GraphGUI::getNumCols( + const int num_proxels, + const int width, + const int height, + const int minimum_window_width +) +{ + for (int num_cols = num_proxels; num_cols > 1; --num_cols) + { + if (getWindowWidth(num_cols, width, height) >= minimum_window_width) + { + return num_cols; + } + } + + return 1; +} + +namespace +{ +std::map getProxelSet( + const Graph& graph, + const std::map& non_blacklisted_statuses +) +{ + std::map proxels; + + for (const auto& kv : non_blacklisted_statuses) + { + const std::string& name = kv.first; + proxels[name] = graph.getProxel(name); + } + + return proxels; +} + +std::map getProxelStatuses( + const Graph& graph, + const std::unordered_set& blacklisted_proxels +) +{ + auto statuses = graph.getProxelStatuses(); + + for (auto it = statuses.begin(); it != statuses.end();) + { + const bool is_blacklisted = blacklisted_proxels.find(it->first) != blacklisted_proxels.end(); + const bool is_crashed = it->second.state == ProxelStatus::State::Crashed; + + if (!is_crashed && is_blacklisted) + { + it = statuses.erase(it); + } + else + { + ++it; + } + } + + return statuses; +} +} +} diff --git a/curses/src/proxel_window.cpp b/curses/src/proxel_window.cpp new file mode 100644 index 0000000..7e0603c --- /dev/null +++ b/curses/src/proxel_window.cpp @@ -0,0 +1,180 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/curses/proxel_window.h" +#include "ncursescpp/ncursescpp.hpp" + +#include +#include + +namespace flow::curses +{ +ProxelWindow::ProxelWindow() + : ProxelWindow{0, 0, 0, 0} +{} + +ProxelWindow::ProxelWindow( + const int x, + const int y, + const int width, + const size_t max_ports_shown +) + : x_{x} + , y_{y} + , width_{width} + , max_ports_shown_{max_ports_shown} + , window_{x, y, width, inner_height} +{} + +void ProxelWindow::renderStatus( + const std::string& name, + const ProxelStatus& status +) +{ + std::vector lines; + + { + std::ostringstream ss; + ss << "state: " << status.state; + + lines.push_back(ss.str()); + } + + { + const size_t max_info = 5; + + const auto max_line_length = static_cast(width_ - 2); + std::vector info_lines = getLines(status.info, max_line_length); + const size_t num_info = std::min(max_info, info_lines.size()); + + std::move( + info_lines.begin(), + std::next(info_lines.begin(), num_info), + std::back_inserter(lines) + ); + } + + const auto color = getStateColor(status.state); + window_.render(name, lines, color); + + { + constexpr int width = 10; + constexpr int height = 2; + int i = 0; + for (const auto& kv : status.ports) + { + if (max_ports_shown_ > 0 && i >= static_cast(max_ports_shown_)) + { + break; + } + + Window port_window( + x_ + i * width + 1, + y_ + inner_height + 1, + width, + height); + + std::vector port_lines; + + const PortStatus& port_status = kv.second; + + if (port_status.num_connections != PortStatus::undefined) + { + std::ostringstream ss; + ss << "C:" + << std::setfill(' ') << std::setw(width - 4) + << port_status.num_connections; + + port_lines.push_back(ss.str()); + } + + if (port_status.num_transactions != PortStatus::undefined) + { + std::ostringstream ss; + ss << "T:" + << std::setfill(' ') << std::setw(width - 4) + << port_status.num_transactions; + + port_lines.push_back(ss.str()); + } + + port_window.render(kv.first, port_lines, color); + ++i; + } + } +} + +int ProxelWindow::getHeight() +{ + return inner_height + 2; +} + +std::vector ProxelWindow::getLines( + const std::string& str, + const size_t max_line_length) +{ + const std::vector raw_lines = split(str, '\n'); + + std::vector lines; + + for (const std::string& raw_line : raw_lines) + { + std::vector chunks = split(raw_line, max_line_length); + + std::move(chunks.begin(), chunks.end(), std::back_inserter(lines)); + } + + return lines; +} + +std::vector ProxelWindow::split( + const std::string& str, + const char sep) +{ + std::istringstream ss{str}; + std::vector lines; + + for (std::string line; std::getline(ss, line, sep);) + { + lines.push_back(std::move(line)); + } + + return lines; +} + +std::vector ProxelWindow::split( + const std::string& str, + const size_t chunk_size) +{ + std::vector chunks; + + for (size_t i = 0; i < str.length(); i += chunk_size) + { + chunks.push_back(str.substr(i, chunk_size)); + } + + return chunks; +} + +nccpp::Color ProxelWindow::getStateColor(const ProxelStatus::State state) +{ + using nccpp::colors::red; + using nccpp::colors::green; + using nccpp::colors::blue; + using nccpp::colors::yellow; + using nccpp::colors::black; + using nccpp::colors::white; + + switch (state) + { + case ProxelStatus::State::Running: + return {green, black}; + case ProxelStatus::State::Crashed: + return {white, red}; + case ProxelStatus::State::AwaitingInput: + return {yellow, black}; + case ProxelStatus::State::Paused: + return {blue, black}; + default: + return {white, black}; + } +} +} diff --git a/curses/src/window.cpp b/curses/src/window.cpp new file mode 100644 index 0000000..cc6b15f --- /dev/null +++ b/curses/src/window.cpp @@ -0,0 +1,137 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/curses/window.h" +#include "superflow/utils/pimpl_impl.h" +#include "ncursescpp/ncursescpp.hpp" +#include +#include + +// https://stackoverflow.com/a/2351155 +// You can use explicit instantiation to create an instantiation of +// a templated class or function without actually using it in your code. +// Because this is useful when you are creating library files that +// use templates for distribution, uninstantiated template definitions +// are not put into object files. +template class flow::pimpl; + +namespace flow::curses +{ +class Window::impl +{ +public: + impl(int x, int y, int width, int height); + + static constexpr int content_col_offset = 1; + static constexpr int header_col_offset = 2; + + int width_; + int height_; + nccpp::Window window_; + + void setColor(const nccpp::Color& color); + + void renderBox(int background_color); + + void renderName(const std::string& name); + + void renderLine(const std::string& line, int row_offset); + + [[nodiscard]] std::string shortenString( + const std::string& str, + int col_offset, + bool pad = false + ) const; +}; + +Window::Window(const int x, const int y, const int width, const int height) + : m_{x, y, width, height} +{} + +void Window::render( + const std::string& name, + const std::vector& lines, + const nccpp::Color& color +) +{ + m_->setColor(color); + m_->renderBox(color.background); + m_->renderName(name); + + for (size_t i = 0; i < lines.size(); ++i) + { + const int row_offset = 1 + static_cast(i); + + m_->renderLine(lines[i], row_offset); + } + + for (auto i = static_cast(lines.size()); i < m_->height_; ++i) + { + const int row_offset = 1 + i; + + m_->renderLine("", row_offset); + } + + m_->window_.refresh(); +} + +Window::impl::impl(const int x, const int y, const int width, const int height) + : width_{width} + , height_{height} + , window_{height + 2, width, y, x} +{} + +void Window::impl::setColor(const nccpp::Color& color) +{ + const auto attr = static_cast(nccpp::ncurses().color_to_attr(color)); + + window_.bkgd(attr); + window_.attron(attr); +} + +void Window::impl::renderBox(const int) +{ + window_.box('|', '-'); +} + +void Window::impl::renderName(const std::string& name) +{ + const std::string header = shortenString(name, header_col_offset); + + window_.mvprintw(0, header_col_offset, header.c_str()); +} + +void Window::impl::renderLine(const std::string& line, int row_offset) +{ + const std::string content = shortenString(line, content_col_offset, true); + + window_.mvprintw(row_offset, content_col_offset, content.c_str()); +} + +std::string Window::impl::shortenString( + const std::string& str, + const int col_offset, + const bool pad) const +{ + const auto width = static_cast(width_ - 2 * col_offset); + std::ostringstream ss; + + if (str.length() <= width) + { + if (!pad || str.length() == width) + { + return str; + } + + ss << str + << std::setfill(' ') << std::setw(static_cast(width - str.length())) << ""; + } else + { + const auto first_half_width = static_cast(width / 2); + const auto last_half_width = width - first_half_width; + + ss << str.substr(0, first_half_width) + << str.substr(str.length() - last_half_width, last_half_width); + } + + return ss.str(); +} +} diff --git a/curses/test/CMakeLists.txt b/curses/test/CMakeLists.txt new file mode 100644 index 0000000..3d058f3 --- /dev/null +++ b/curses/test/CMakeLists.txt @@ -0,0 +1,22 @@ +set(PARENT_PROJECT ${PROJECT_NAME}) +set(target_test_name "${CMAKE_PROJECT_NAME}-${PARENT_PROJECT}-test") +project(${target_test_name} CXX) +message(STATUS "* Adding test executable '${target_test_name}'") + +add_executable(${target_test_name} + "test_superflow_curses.cpp" +) + +target_link_libraries( + ${target_test_name} + PRIVATE GTest::gtest GTest::gtest_main + PRIVATE ${CMAKE_PROJECT_NAME}::curses +) + +set_target_properties(${target_test_name} PROPERTIES + CXX_STANDARD_REQUIRED ON + CXX_STANDARD 17 +) + +include(GoogleTest) +gtest_discover_tests(${target_test_name}) diff --git a/curses/test/test_superflow_curses.cpp b/curses/test/test_superflow_curses.cpp new file mode 100644 index 0000000..9158e93 --- /dev/null +++ b/curses/test/test_superflow_curses.cpp @@ -0,0 +1,14 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/curses/graph_gui.h" +#include "superflow/graph.h" + +#include "gtest/gtest.h" + +using namespace flow; +using namespace flow::curses; + +TEST(SuperFlowCurses, compiles) +{ + Graph graph(std::map{}); + GraphGUI gui; +} diff --git a/doc/Doxyfile b/doc/Doxyfile new file mode 100644 index 0000000..6848f6f --- /dev/null +++ b/doc/Doxyfile @@ -0,0 +1,338 @@ +# Doxyfile 1.9.1 + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = Superflow +PROJECT_NUMBER = $(superflow_VERSION) +PROJECT_BRIEF = +PROJECT_LOGO = +OUTPUT_DIRECTORY = ./doc +CREATE_SUBDIRS = NO +ALLOW_UNICODE_NAMES = NO +OUTPUT_LANGUAGE = English +OUTPUT_TEXT_DIRECTION = None +BRIEF_MEMBER_DESC = YES +REPEAT_BRIEF = YES +ABBREVIATE_BRIEF = +ALWAYS_DETAILED_SEC = NO +INLINE_INHERITED_MEMB = NO +FULL_PATH_NAMES = YES +STRIP_FROM_PATH = +STRIP_FROM_INC_PATH = +SHORT_NAMES = NO +JAVADOC_AUTOBRIEF = YES +JAVADOC_BANNER = NO +QT_AUTOBRIEF = NO +MULTILINE_CPP_IS_BRIEF = NO +PYTHON_DOCSTRING = YES +INHERIT_DOCS = YES +SEPARATE_MEMBER_PAGES = NO +TAB_SIZE = 2 +ALIASES = +OPTIMIZE_OUTPUT_FOR_C = NO +OPTIMIZE_OUTPUT_JAVA = NO +OPTIMIZE_FOR_FORTRAN = NO +OPTIMIZE_OUTPUT_VHDL = NO +OPTIMIZE_OUTPUT_SLICE = NO +EXTENSION_MAPPING = +MARKDOWN_SUPPORT = YES +TOC_INCLUDE_HEADINGS = 5 +AUTOLINK_SUPPORT = YES +BUILTIN_STL_SUPPORT = NO +CPP_CLI_SUPPORT = NO +SIP_SUPPORT = NO +IDL_PROPERTY_SUPPORT = YES +DISTRIBUTE_GROUP_DOC = NO +GROUP_NESTED_COMPOUNDS = NO +SUBGROUPING = YES +INLINE_GROUPED_CLASSES = NO +INLINE_SIMPLE_STRUCTS = NO +TYPEDEF_HIDES_STRUCT = NO +LOOKUP_CACHE_SIZE = 0 +NUM_PROC_THREADS = 1 +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- +EXTRACT_ALL = YES +EXTRACT_PRIVATE = NO +EXTRACT_PRIV_VIRTUAL = NO +EXTRACT_PACKAGE = YES +EXTRACT_STATIC = YES +EXTRACT_LOCAL_CLASSES = YES +EXTRACT_LOCAL_METHODS = NO +EXTRACT_ANON_NSPACES = NO +RESOLVE_UNNAMED_PARAMS = YES +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO +HIDE_FRIEND_COMPOUNDS = NO +HIDE_IN_BODY_DOCS = NO +INTERNAL_DOCS = NO +CASE_SENSE_NAMES = YES +HIDE_SCOPE_NAMES = NO +HIDE_COMPOUND_REFERENCE= NO +SHOW_INCLUDE_FILES = YES +SHOW_GROUPED_MEMB_INC = NO +FORCE_LOCAL_INCLUDES = NO +INLINE_INFO = YES +SORT_MEMBER_DOCS = YES +SORT_BRIEF_DOCS = YES +SORT_MEMBERS_CTORS_1ST = YES +SORT_GROUP_NAMES = NO +SORT_BY_SCOPE_NAME = NO +STRICT_PROTO_MATCHING = NO +GENERATE_TODOLIST = YES +GENERATE_TESTLIST = YES +GENERATE_BUGLIST = YES +GENERATE_DEPRECATEDLIST= YES +ENABLED_SECTIONS = +MAX_INITIALIZER_LINES = 30 +SHOW_USED_FILES = NO +SHOW_FILES = NO +SHOW_NAMESPACES = YES +FILE_VERSION_FILTER = +LAYOUT_FILE = +CITE_BIB_FILES = +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- +QUIET = NO +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_NO_PARAMDOC = NO +WARN_AS_ERROR = NO +WARN_FORMAT = "$file:$line: $text" +WARN_LOGFILE = +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- +INPUT = README.md \ + core \ + curses \ + loader \ + yaml +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = +RECURSIVE = YES +EXCLUDE = 3rd-party +EXCLUDE_SYMLINKS = NO +EXCLUDE_PATTERNS = */test/* +EXCLUDE_SYMBOLS = +EXAMPLE_PATH = +EXAMPLE_PATTERNS = +EXAMPLE_RECURSIVE = NO +IMAGE_PATH = +INPUT_FILTER = +FILTER_PATTERNS = +FILTER_SOURCE_FILES = NO +FILTER_SOURCE_PATTERNS = +USE_MDFILE_AS_MAINPAGE = ./README.md +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- +SOURCE_BROWSER = NO +INLINE_SOURCES = NO +STRIP_CODE_COMMENTS = YES +REFERENCED_BY_RELATION = NO +REFERENCES_RELATION = NO +REFERENCES_LINK_SOURCE = YES +SOURCE_TOOLTIPS = YES +USE_HTAGS = NO +VERBATIM_HEADERS = YES +CLANG_ASSISTED_PARSING = NO +CLANG_ADD_INC_PATHS = YES +CLANG_OPTIONS = +CLANG_DATABASE_PATH = +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- +ALPHABETICAL_INDEX = YES +IGNORE_PREFIX = +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- +GENERATE_HTML = YES +HTML_OUTPUT = html +HTML_FILE_EXTENSION = .html +HTML_HEADER = +HTML_FOOTER = +HTML_STYLESHEET = +HTML_EXTRA_STYLESHEET = +HTML_EXTRA_FILES = +HTML_COLORSTYLE_HUE = 220 +HTML_COLORSTYLE_SAT = 100 +HTML_COLORSTYLE_GAMMA = 80 +HTML_TIMESTAMP = YES +HTML_DYNAMIC_MENUS = YES +HTML_DYNAMIC_SECTIONS = NO +HTML_INDEX_NUM_ENTRIES = 100 +GENERATE_DOCSET = NO +DOCSET_FEEDNAME = "Doxygen generated docs" +DOCSET_BUNDLE_ID = org.doxygen.Project +DOCSET_PUBLISHER_ID = org.doxygen.Publisher +DOCSET_PUBLISHER_NAME = Publisher +GENERATE_HTMLHELP = NO +CHM_FILE = +HHC_LOCATION = +GENERATE_CHI = NO +CHM_INDEX_ENCODING = +BINARY_TOC = NO +TOC_EXPAND = NO +GENERATE_QHP = NO +QCH_FILE = +QHP_NAMESPACE = org.doxygen.Project +QHP_VIRTUAL_FOLDER = doc +QHP_CUST_FILTER_NAME = +QHP_CUST_FILTER_ATTRS = +QHP_SECT_FILTER_ATTRS = +QHG_LOCATION = +GENERATE_ECLIPSEHELP = NO +ECLIPSE_DOC_ID = org.doxygen.Project +DISABLE_INDEX = NO +GENERATE_TREEVIEW = NO +ENUM_VALUES_PER_LINE = 4 +TREEVIEW_WIDTH = 250 +EXT_LINKS_IN_WINDOW = NO +HTML_FORMULA_FORMAT = png +FORMULA_FONTSIZE = 10 +FORMULA_TRANSPARENT = YES +FORMULA_MACROFILE = +USE_MATHJAX = YES +MATHJAX_FORMAT = HTML-CSS +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest +MATHJAX_EXTENSIONS = +MATHJAX_CODEFILE = +SEARCHENGINE = YES +SERVER_BASED_SEARCH = NO +EXTERNAL_SEARCH = NO +SEARCHENGINE_URL = +SEARCHDATA_FILE = searchdata.xml +EXTERNAL_SEARCH_ID = +EXTRA_SEARCH_MAPPINGS = +#--------------------------------------------------------------------------- +# Configuration options related to the LaTeX output +#--------------------------------------------------------------------------- +GENERATE_LATEX = NO +LATEX_OUTPUT = latex +LATEX_CMD_NAME = latex +MAKEINDEX_CMD_NAME = makeindex +LATEX_MAKEINDEX_CMD = makeindex +COMPACT_LATEX = NO +PAPER_TYPE = a4 +EXTRA_PACKAGES = +LATEX_HEADER = +LATEX_FOOTER = +LATEX_EXTRA_STYLESHEET = +LATEX_EXTRA_FILES = +PDF_HYPERLINKS = YES +USE_PDFLATEX = YES +LATEX_BATCHMODE = NO +LATEX_HIDE_INDICES = NO +LATEX_SOURCE_CODE = NO +LATEX_BIB_STYLE = plain +LATEX_TIMESTAMP = NO +LATEX_EMOJI_DIRECTORY = +#--------------------------------------------------------------------------- +# Configuration options related to the RTF output +#--------------------------------------------------------------------------- +GENERATE_RTF = NO +RTF_OUTPUT = rtf +COMPACT_RTF = NO +RTF_HYPERLINKS = NO +RTF_STYLESHEET_FILE = +RTF_EXTENSIONS_FILE = +RTF_SOURCE_CODE = NO +#--------------------------------------------------------------------------- +# Configuration options related to the man page output +#--------------------------------------------------------------------------- +GENERATE_MAN = NO +MAN_OUTPUT = man +MAN_EXTENSION = .3 +MAN_SUBDIR = +MAN_LINKS = NO +#--------------------------------------------------------------------------- +# Configuration options related to the XML output +#--------------------------------------------------------------------------- +GENERATE_XML = NO +XML_OUTPUT = xml +XML_PROGRAMLISTING = YES +XML_NS_MEMB_FILE_SCOPE = NO +#--------------------------------------------------------------------------- +# Configuration options related to the DOCBOOK output +#--------------------------------------------------------------------------- +GENERATE_DOCBOOK = NO +DOCBOOK_OUTPUT = docbook +DOCBOOK_PROGRAMLISTING = NO +#--------------------------------------------------------------------------- +# Configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- +GENERATE_AUTOGEN_DEF = NO +#--------------------------------------------------------------------------- +# Configuration options related to the Perl module output +#--------------------------------------------------------------------------- +GENERATE_PERLMOD = NO +PERLMOD_LATEX = NO +PERLMOD_PRETTY = YES +PERLMOD_MAKEVAR_PREFIX = +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = NO +EXPAND_ONLY_PREDEF = NO +SEARCH_INCLUDES = YES +INCLUDE_PATH = +INCLUDE_FILE_PATTERNS = +PREDEFINED = +EXPAND_AS_DEFINED = +SKIP_FUNCTION_MACROS = YES +#--------------------------------------------------------------------------- +# Configuration options related to external references +#--------------------------------------------------------------------------- +TAGFILES = doc/cppreference-doxygen-web.tag=http://en.cppreference.com/w/ +GENERATE_TAGFILE = doc/superflow.tag +ALLEXTERNALS = NO +EXTERNAL_GROUPS = YES +EXTERNAL_PAGES = YES +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- +CLASS_DIAGRAMS = YES +DIA_PATH = +HIDE_UNDOC_RELATIONS = YES +HAVE_DOT = YES +DOT_NUM_THREADS = 0 +DOT_FONTNAME = Helvetica +DOT_FONTSIZE = 10 +DOT_FONTPATH = +CLASS_GRAPH = YES +COLLABORATION_GRAPH = YES +GROUP_GRAPHS = YES +UML_LOOK = NO +UML_LIMIT_NUM_FIELDS = 10 +DOT_UML_DETAILS = NO +DOT_WRAP_THRESHOLD = 17 +TEMPLATE_RELATIONS = NO +INCLUDE_GRAPH = YES +INCLUDED_BY_GRAPH = YES +CALL_GRAPH = YES +CALLER_GRAPH = YES +GRAPHICAL_HIERARCHY = YES +DIRECTORY_GRAPH = YES +DOT_IMAGE_FORMAT = png +INTERACTIVE_SVG = NO +DOT_PATH = +DOTFILE_DIRS = +MSCFILE_DIRS = +DIAFILE_DIRS = +PLANTUML_JAR_PATH = +PLANTUML_CFG_FILE = +PLANTUML_INCLUDE_PATH = +DOT_GRAPH_MAX_NODES = 50 +MAX_DOT_GRAPH_DEPTH = 0 +DOT_TRANSPARENT = NO +DOT_MULTI_TARGETS = YES +GENERATE_LEGEND = YES +DOT_CLEANUP = YES diff --git a/doc/img/superflow-graph.png b/doc/img/superflow-graph.png new file mode 100644 index 0000000000000000000000000000000000000000..2e46ebb0902f33f39a034f5a19c3678f91447b0e GIT binary patch literal 116422 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 X!Dp{1b#LQW{8B=9spsu+S$jXosN1i=P&if21Y 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 literal 0 HcmV?d00001 diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt new file mode 100644 index 0000000..3a60989 --- /dev/null +++ b/loader/CMakeLists.txt @@ -0,0 +1,24 @@ +project(loader) +init_module() + +find_package(Boost REQUIRED system filesystem) + +add_library_boilerplate() + +target_link_libraries(${target_name} + PUBLIC + Boost::headers + Boost::filesystem + Boost::system + superflow::core + INTERFACE + ${CMAKE_DL_LIBS} + ) + +if (MSVC) + target_link_libraries(${target_name} INTERFACE Boost::dynamic_linking) +endif() + +if (BUILD_TESTS) + add_subdirectory(test) +endif() diff --git a/loader/README.md b/loader/README.md new file mode 100644 index 0000000..7e517e5 --- /dev/null +++ b/loader/README.md @@ -0,0 +1,103 @@ + +### loader +The `loader` module enables dynamic loading of proxel libraries (shared libraries) like plugins. +A `loader`-compatible library has embedded a list of Factorys for the proxels it contains, +which assists in the creation of a corresponding _FactoryMap_. +The advantage of this is that the user doesn't have to include any specific header files from the +library in their consuming application, and with the addition of the `yaml` module, +no hard coded proxel names are required at all in the compiled client code. + +There are three important aspects when dealing with proxel libraries: + +1. When compiling the proxel library, you must have already decided on a concrete `PropertyList` (or "value adapter"). + At the time of writing, the `flow::yaml::YAMLPropertyList` is the only PropertyList we have implemented. +2. For each proxel, the factory must be "registered" using the macro `REGISTER_PROXEL_FACTORY` + from the header file `"superflow/loader/register_factory.h"`. +3. When loading the library into the consuming application, create a `flow::load::ProxelLibrary` for each shared library, + and keep them in scope as long as their proxels are in use. + +#### 1. PropertyList +The interface of a `PropertyList` is defined through the templated function +`flow::value` from `"superflow/value.h"`. +It is required that a valid `PropertyList` defines the following methods: + +- `bool hasKey(const std::string& key)`, which tells if the given key exists or not. +- `T convertValue(const std::string& key)`, which retrieves a value from the list. + +In addition, `flow::load::loadFactories` demands the existence of the static string `PropertyList::adapter_name`. +It must contain the value of the macro `LOADER_ADAPTER_NAME` (see the next section). + +#### 2. REGISTER_PROXEL_FACTORY +This macro will export a _named symbol_ to a _named section_ in the shared library, +which can later be retrieved by those names. See the `Boost::DLL` documentation for more details. + +For the user, we want this to be as pleasant an experience as possible: + +```cpp +#include "my/proxel.h" +#include "superflow/loader/register_factory.h" + +template +flow::Proxel::Ptr createMyProxel(const PropertyList& adapter) +{ + return std::make_shared( + flow::value(adapter, "key") + ); +} + +REGISTER_PROXEL_FACTORY(MyProxel, createMyProxel) +``` + +In order for this to work, we demand that the author of the PropertyList defines the following macros: + +- `LOADER_ADAPTER_HEADER`, path to the concrete property list header file +- `LOADER_ADAPTER_NAME`, a short, unique identifier for the property list +- `LOADER_ADAPTER_TYPE`, the concrete type of property list + +For the `flow::yaml` module, these macros are defined through the target's "interface compile definitions". +That means that when you link your proxel library to `flow::yaml`, they will be automatically defined. + +#### 3. ProxelLibrary + +A proxel library and its _FactoryMap_ are accessed through the use of a `flow::load::ProxelLibrary` object. +You construct it using the path to the shared library file, and fetch the _FactoryMap_ using the method `loadFactories`. +An object of this class must not go out of scope as long as its proxels are in use. +That will cause the shared library to be unloaded, and your application to crash! + +```cpp +const flow::load::ProxelLibrary library{"path/to/library"}; +const auto factories = library.loadFactories(); +``` + +You can collect factories from multiple libraries using the free function `loadFactories`. +Remember that the libraries mustn't go out of scope, hence we keep the vector. + +```cpp +const std::vector library_paths{ + {"path/to/library1"}, + {"path/to/library2"} + }; + +const auto factories = flow::load::loadFactories( + library_paths +); +``` + +##### Pro tip +If you separate the name of the library and the path to the directory in which the library resides, +Boost can automatically determine the prefix and postfix of the shared library file. + +```cpp +const std::string library_directory{"/directory"}; +const std::string library_name{"myproxels"}; +const flow::load::ProxelLibrary library{library_directory, library_name}; + +/// Linux result: /directory/libmyproxels.so +/// Windows result: /directory/myproxels.dll +``` + +#### Further reading + +See the tests in the `loader` module for more examples, or read the report for more details. + +--- diff --git a/loader/include/superflow/loader/load_factories.h b/loader/include/superflow/loader/load_factories.h new file mode 100644 index 0000000..b0a7d38 --- /dev/null +++ b/loader/include/superflow/loader/load_factories.h @@ -0,0 +1,40 @@ +#pragma once + +#include "superflow/loader/proxel_library.h" + +namespace flow::load +{ +/// \brief Collect factories across multiple ProxelLibrary%s and concatenate into a single FactoryMap +/// \see ProxelLibrary::loadFactories +template +FactoryMap loadFactories( + const std::vector& libraries, + Args... args +) +{ + FactoryMap factories; + + for (const auto& library : libraries) + { + factories += library.loadFactories(args...); + } + + return factories; +} + +// It is disallowed to construct the vector as a temporary within the function argument list, +// as this would cause the library to be unloaded immediately after return. +// The std::is_same is there to make the static_assert dependent on the template parameter, +// otherwise it would cause a compilation error even when the overload is not called. +template +FactoryMap loadFactories( + const std::vector&&, + Args... +) +{ + static_assert( + !std::is_same::value, // always false + "No temporary allowed!" + "You must keep the libraries in scope, so they won't be unloaded while you attempt to use them."); +} +} diff --git a/loader/include/superflow/loader/proxel_library.h b/loader/include/superflow/loader/proxel_library.h new file mode 100644 index 0000000..d453ad1 --- /dev/null +++ b/loader/include/superflow/loader/proxel_library.h @@ -0,0 +1,80 @@ +// Copyright 2023, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/factory_map.h" +#include "boost/dll/shared_library.hpp" +#include + +namespace flow::load +{ +/// This class will hold a shared library that contains proxels and proxel factories, +/// which can be dynamically loaded using the flow::load module. +/// \note An object of this class must not go out of scope as long as its proxels are in use. +/// That will cause the shared library to be unloaded, and your application to crash! +class ProxelLibrary +{ +public: + /// Load a proxel library from a path, and the library name. + /// The prefix and suffix of the library file will be computed based on platform (more portable code) + /// E.g.: library name: myproxels + /// Linux result: libmyproxels.so + /// Windows result: myproxels.dll + ProxelLibrary( + const boost::dll::fs::path& path_to_directory, + const std::string& library_name + ); + + /// Load a proxel library by using the full path to the exact library file. + ProxelLibrary( + const boost::dll::fs::path& full_path + ); + + /// \brief Load the factories registered with type PropertyList + /// \tparam PropertyList Some class compatible with flow::value + /// \tparam Args any optional arguments to the factories (see unit tests for examples) + /// \param args any optional arguments to the factories (see unit tests for examples) + /// \return the factories + /// \throws std::invalid_argument if there are no string PropertyList::adapter_name, + /// or there are no proxel factories registered to that adapter name in the library. + template + FactoryMap loadFactories( + Args... args + ) const; + +private: + /// Holds the actual shared library + boost::dll::shared_library library_; + + /// Exception handling are covered here + ProxelLibrary( + const boost::dll::fs::path& path, + boost::dll::load_mode::type load_mode + ); + + [[nodiscard]] std::vector getSectionSymbols(const std::string& adapter_name) const; + + /// All factory-symbols are prefixed with the adapter_name + underscore + [[nodiscard]] static std::string extractFactoryName(const std::string& symbol, const std::string& adapter_name); +}; + +// -- ProxelLibrary::loadFactories implementation -- // +template +FactoryMap ProxelLibrary::loadFactories(Args... args) const +{ + typename std::map> factories; + + const auto symbols = getSectionSymbols(PropertyList::adapter_name); + for (const auto& symbol: symbols) + { + auto factory = library_.get_alias < flow::Proxel::Ptr(PropertyList const&, Args...)>(symbol); + factories.emplace( + extractFactoryName(symbol, PropertyList::adapter_name), + [factory = std::move(factory), args...](PropertyList const& adapter) + { + return std::invoke(factory, adapter, args...); + } + ); + } + return FactoryMap{std::move(factories)}; +} +} diff --git a/loader/include/superflow/loader/register_factory.h b/loader/include/superflow/loader/register_factory.h new file mode 100644 index 0000000..e75a069 --- /dev/null +++ b/loader/include/superflow/loader/register_factory.h @@ -0,0 +1,77 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +/// \file register-factory.h +/// \brief Macros for creating proxel plugin libraries. +/// +/// See #REGISTER_PROXEL_FACTORY(ProxelName, Factory). + +/// \def REGISTER_PROXEL_FACTORY(ProxelName, Factory) +/// Register the symbol of the Factory function \a Factory to the alias \a ProxelName. +/// +/// The macro \a LOADER_ADAPTER_NAME must be defined and will be used as the +/// shared library section name. +/// +/// The macro \a LOADER_ADAPTER_TYPE must be defined and set to a +/// valid implementation of ValueAdapter when compiling the proxel plugin library. +/// +/// The macro \a LOADER_ADAPTER_HEADER may be defined and set to a +/// header file containing the definition of LOADER_ADAPTER_TYPE. +/// +/// \note The library flow::yaml will define the macros in its cmake INTERFACE_COMPILE_DEFINITIONS, +/// so you should not have to worry about them. +/// Thus, the following example is contrived, but illustrates the mechanism: +/// +/// \code{.cpp} +/// #define LOADER_ADAPTER_NAME YAML +/// #define LOADER_ADAPTER_TYPE flow::yaml::YAMLPropertyList +/// #define LOADER_ADAPTER_HEADER "superflow/yaml/yaml_property_list.h" +/// // ... +/// REGISTER_PROXEL_FACTORY(MyProxel, createMyProxel) +/// +/// \param ProxelName The class name of the Proxel you want to register a factory for. +/// \param Factory The flow::Factory function that can create a \a ProxelName wrapped in a Proxel::Ptr. + +/// \def LOADER_ADAPTER_TYPE +/// The actual type of PropertyList, e.g. flow::yaml::YAMLPropertyList + +#pragma once + +#include "boost/dll/alias.hpp" +#include "boost/preprocessor/stringize.hpp" + +#ifdef LOADER_ADAPTER_HEADER +#include LOADER_ADAPTER_HEADER +#endif + +/// Helper macro +#define ALIAS(SECTION, NAME) \ + BOOST_PP_CAT(BOOST_PP_CAT(SECTION, _), NAME) + +/// \brief Register the symbol of the Factory function \a Factory to the alias \a ProxelName +/// within the shared library section \a Section. +/// Helper macros like REGISTER_PROXEL_FACTORY, or macros for specific adapters like +/// REGISTER_YAML_PROXEL_FACTORY (defined elsewhere) +/// will call this macro with a predefined section name. +/// Typically not used directly, but an example would be +/// +/// \code{.cpp} +/// REGISTER_PROXEL_FACTORY_SECTIONED(YAML_MyProxel, createMyProxel, YAML) +/// +#define REGISTER_PROXEL_FACTORY_SECTIONED(ProxelName, Factory, Section) \ + BOOST_DLL_ALIAS_SECTIONED( \ + Factory, \ + ALIAS(Section, ProxelName), \ + Section \ + ) + +#ifdef LOADER_IGNORE +#define REGISTER_PROXEL_FACTORY(ProxelName, Factory) +#elif defined(LOADER_ADAPTER_NAME) && defined(LOADER_ADAPTER_TYPE) +#define REGISTER_PROXEL_FACTORY(ProxelName, Factory) \ + REGISTER_PROXEL_FACTORY_SECTIONED(ProxelName, Factory, LOADER_ADAPTER_NAME) +#else +#define REGISTER_PROXEL_FACTORY(ProxelName, Factory) \ + _Pragma(BOOST_PP_STRINGIZE(message \ + "LOADER_ADAPTER_NAME and LOADER_ADAPTER_TYPE are not defined. "\ + "REGISTER_PROXEL_FACTORY(" BOOST_PP_STRINGIZE(ProxelName) ", " BOOST_PP_STRINGIZE(Factory) ") has no effect."\ + )) +#endif diff --git a/loader/loader-config.cmake.in b/loader/loader-config.cmake.in new file mode 100644 index 0000000..f5019ff --- /dev/null +++ b/loader/loader-config.cmake.in @@ -0,0 +1,9 @@ +@PACKAGE_INIT@ +message(STATUS "* Found @CMAKE_PROJECT_NAME@::@PROJECT_NAME@: " "${CMAKE_CURRENT_LIST_FILE}") +include(CMakeFindDependencyMacro) +find_dependency(Boost COMPONENTS system filesystem) + +set(@CMAKE_PROJECT_NAME@_@PROJECT_NAME@_FOUND TRUE) + +check_required_components(@PROJECT_NAME@) +message(STATUS "* Loading @CMAKE_PROJECT_NAME@::@PROJECT_NAME@ complete") diff --git a/loader/src/proxel_library.cpp b/loader/src/proxel_library.cpp new file mode 100644 index 0000000..d634ae6 --- /dev/null +++ b/loader/src/proxel_library.cpp @@ -0,0 +1,62 @@ +#include "superflow/loader/proxel_library.h" +#include "boost/dll/library_info.hpp" + +namespace flow::load +{ + +ProxelLibrary::ProxelLibrary(const boost::dll::fs::path& path_to_directory, const std::string& library_name) + : ProxelLibrary{ + path_to_directory / library_name, + boost::dll::load_mode::append_decorations | boost::dll::load_mode::rtld_now} +{} + +ProxelLibrary::ProxelLibrary(const boost::dll::fs::path& full_path) + : ProxelLibrary{ + full_path, + boost::dll::load_mode::rtld_now} +{} + +ProxelLibrary::ProxelLibrary(const boost::dll::fs::path& path, boost::dll::load_mode::type load_mode) +try + : library_{ + path, + load_mode} +{} +catch (boost::system::system_error& e) +{ + const auto error_code = e.code(); + const auto error_code_value = std::to_string(error_code.value()); + const std::string error_code_category_name{error_code.category().name()}; + const auto error_code_message = error_code.message(); + const std::string what = + "Exception occurred in constructor 'ProxelLibrary(\"" + path.string() + "\")':\n " + + "error_code " + error_code_value + " (" + error_code_category_name + ") message: " + error_code_message + ". What:\n " + + e.what(); + + throw std::invalid_argument(what); +} +catch (std::ios_base::failure& e) +{ + throw std::invalid_argument( + "Exception occurred in constructor 'ProxelLibrary(\"" + path.string() + "\")', probably due to non-existing path.\n " + + e.what()); +} + +std::vector ProxelLibrary::getSectionSymbols(const std::string& adapter_name) const +{ + boost::dll::library_info info(library_.location()); + std::vector symbols = info.symbols(adapter_name); + if (symbols.empty()) + { + throw std::invalid_argument( + "no section '" + adapter_name + "' found in library, or no proxel factories in in it." + ); + } + return symbols; +} + +std::string ProxelLibrary::extractFactoryName(const std::string& symbol, const std::string& adapter_name) +{ + return symbol.substr(adapter_name.size() + 1); +} +} \ No newline at end of file diff --git a/loader/test/CMakeLists.txt b/loader/test/CMakeLists.txt new file mode 100644 index 0000000..585f640 --- /dev/null +++ b/loader/test/CMakeLists.txt @@ -0,0 +1,40 @@ +set(PARENT_PROJECT ${PROJECT_NAME}) +set(target_test_name "${CMAKE_PROJECT_NAME}-${PARENT_PROJECT}-test") +project(${target_test_name} CXX) +message(STATUS "* Adding test executable '${target_test_name}'") + +add_subdirectory(proxels) + + +add_executable(${target_test_name} + "test_load_factories.cpp" + "test_proxel_library.cpp" + "$<$:test_rtld_now.cpp>" +) + +target_link_libraries( + ${target_test_name} + PRIVATE + GTest::gtest GTest::gtest_main + ${CMAKE_PROJECT_NAME}::core + ${CMAKE_PROJECT_NAME}::loader + superflow::dummy-adapter + ) + +set_target_properties(${target_test_name} PROPERTIES + CXX_STANDARD_REQUIRED ON + CXX_STANDARD 17 + ) + +file(GENERATE + OUTPUT "$/libdependent_library.so" + INPUT "${CMAKE_CURRENT_LIST_DIR}/libs/libdependent_library.so" +) + +file(GENERATE + OUTPUT "$/libmissing_dependency.so" + INPUT "${CMAKE_CURRENT_LIST_DIR}/libs/libmissing_dependency.so" +) + +include(GoogleTest) +gtest_discover_tests(${target_test_name}) diff --git a/loader/test/README.md b/loader/test/README.md new file mode 100644 index 0000000..c729768 --- /dev/null +++ b/loader/test/README.md @@ -0,0 +1,55 @@ +How the dummy so-files for testing rtld_now were created: (main is not required) + +```bash +g++ -c -Wall -fpic missing_dependency.cpp +g++ -shared -o libmissing_dependency.so missing_dependency.o + +g++ -c -Wall -fpic dependent_library.cpp +g++ -L. -shared -o libdependent_library.so dependent_library.o -lmissing_dependency + +LD_LIBRARY_PATH=${PWD} g++ -L. -Wall -o main main.cpp -ldependent_library +LD_LIBRARY_PATH=${PWD} ./main + +cp libmissing_dependency.so superflow/loader/test/libs/ +cp libdependent_library.so superflow/loader/test/libs/ +``` + +**missing_dependency.hpp** +```cpp +#pragma once + +void function_in_missing_dependency(); +``` + +**missing_dependency.cpp** +```cpp +#include "missing_dependency.hpp" +#include + +void function_in_missing_dependency() +{ std::cout << "Hello, I am a function in a missing shared library" << std::endl; } +``` + +**dependent_library.hpp** +```cpp +#pragma once + +void dependent_function(); +``` + +**dependent_library.cpp** +```cpp +#include "dependent_library.hpp" +#include "missing_dependency.hpp" + +void dependent_function() +{ function_in_missing_dependency(); } +``` + +**main.cpp** +```cpp +#include "dependent_library.hpp" + +int main() +{ dependent_function(); } +``` diff --git a/loader/test/libs/libdependent_library.so b/loader/test/libs/libdependent_library.so new file mode 100755 index 0000000000000000000000000000000000000000..337b20fe7a079e11c18b461100bb4b1179d166f2 GIT binary patch literal 15936 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 W5@kZ9H-U8ji(d*Iu8SWa>Hjao+HD2^ literal 0 HcmV?d00001 diff --git a/loader/test/libs/libmissing_dependency.so b/loader/test/libs/libmissing_dependency.so new file mode 100755 index 0000000000000000000000000000000000000000..cd42575f56414738b1851e73631a5a0afc3b5a74 GIT binary patch literal 16832 zcmeHOe~c7Y9e;bv!69%*5tJXbOdE_KFzmuXuauKH?)KQVcl3IDm|Ba&-0t4pH2W(% zv%nRvQqMLLLPE9u$A8plq9!J2G_i@sf+%8Y6cRNW(|FookA&zIm0X*K_4|GE{Vp@J zvx@xTk7V9u@B6-=_xEbcrSK z|32|PDZAiyQ$y`DiyVllAiL= zVy4?m50k$MqO6B3>dlkhJn1pLO#Ya*Ic8-av7cWfdw!~r98#DTc%$5!nfm}P7m~){gMLBs3(@f2kpxK%L{6g7(n9HCx?HI zv{~tgh6e%~zX#Zg0Q^C46ogMVk-ymlp9K7#0Q-xY*vD8Y0rGg=LHL#?`1&UH4}$y( z(Jm&bQ@endMc2m*rJ`=xM#a{3p{GU$^=z(^8#gUGR~Z@X&6kR~5o0u;bJ~Ji^vp3s zA2W+a-h3JapV$!_s}?i136{;GUN9}oERKWSa;})o6*H4ZKqhT>W=d6C*78;=ZO7x8 z38SLh6~nZxR3?!a25iTuVVN1dWZ9LRQAlM*^hA26KJ-Y&u>kh+smz_}j_4yX(B1`C z`NV;ADQ_i)`iIk{w4RWF-DQ?69o*$QQ$^G6jVG{|2hu*gPvY`|4Lf(tv_;+=wd`zW z^Jdj5sk@NPIJNx)seQeVKd$dkJB6(4+tuyrPKp(iz3zVP0mK)dpP^;C>NFEn% zH?Cc&^SEHUaqWDa#|6`kYu{oXtPM(QeL~s;{ra4pae*kvr=M+CHnC5%8AfiMDL1i}b}5eOp?Mj(tp7=bVXVFdo~B9P4G z^QA3HN-+wGq3}yK#Vi6xuh|p}-lFBQ3cQD^7!`OC8Ce&7xC^cn-@jb{ORY8nbkAI^ z_6eW^Kwku!c(Ydf2GHX`e+Kjl(Ca|)9W9o~)59WiYPCk_SGNmSaP)OgxI@w_Xl@ukgr`)AGYJ-Wst*nT3ir+5r%{Ah_~1CHN5sa1V67{9E>P##HmN&ggKx8 zceh62q<1+`{@wNG-z&dT@N@6QSBaqU78UOid=A(v4^OG)Mx1(7tQI`}+^tcuMreWZ zJR9AX#>aYrcoWV$cni*ZEpV>=oG;GpgwOf#`w8cHm90Gpm58f9G>wQ}i8sdQDYC=! zHQPBNyR@p)U?Tp={P za9;`Pk28NZj;nRxAQEsNHv--w8po|F@y6$Eka%PM!y8THBQ3#p`kUZc!0A?bdUd>s{1*XNJdzIc{U-RUP4KHt?2Amrw(RQI zn3@r{(>Zjzpl2|l!xFllE$QR=(x{Qwvv#Rs=|=UK$dn4@e9q2g)mX3uCV7~;QK=Y{ zdah{0&&y*Kqma|H)k0wsR9qZnewemL6mvcXx^&$vVG|@*bjEeeJh36c0}&I`M}UK@ z5u=!ebP-7Us5)6BG1C>@pp zBrw@GjMrtA^a=2QIW2fSLV6c^-$xJh45oTP0`gwi06BfMzdxOT43eIG0|_+iPI%EY zcXo%&Y`Lqv6tY7ZBX^WV^(fR#g0t(3Bc zZ3s0tK@ZbJ7OXMn*l?aIM}zC&$|#s%3{0bC=%utXYFR>s=ehuoYrur_Z$h|lz_m9_ zJF@WlkEud96JD1y4@Vk3`n*nLx`73W`q$q*fZv3*Is51R2vgQ)J-`1)Kp*!atk3%u zrd{xTfCaY!SY>_QN2CFZ!iV*Feae*gJ0KzpnPXOb3~IRdV0~W4GUb&I_n+mMPJlk{ zWtivnE>n#>VgFHv`%kbq3Kg`?`n)b?%KInw&-#4*&yxN=GRXTeriTefefZX{`)B=A zfMHDPxnR6LBg*?y5RrxZ!Tj@3aqIJXoaqn?67`uEFZlF%-OiNd*$necKkw7$^*vLK z6^XJQ(=Yq<&rt)@kNXA;>#^Cd`}BFgz?Ap>EbousS<>hGFBFm_x`S9IeE*sME>y6K ztk3%orh8bBsNesOefqplVT$Wpck%oG1vKK?9-l!h{CfuHC;Q{Y`b>WZIvwy~ect!X zQNB3Kp=~V8GkpUv9J{Q~`y + static flow::Proxel::Ptr static_create(const PropertyList&); +}; + +template +flow::Proxel::Ptr Dummy::static_create(const PropertyList&) +{ + return std::make_shared(); +} + +REGISTER_DUMMY_PROXEL_FACTORY(Dummy, Dummy::static_create) +} diff --git a/loader/test/proxels/dummy_value_adapter.cpp b/loader/test/proxels/dummy_value_adapter.cpp new file mode 100644 index 0000000..5177cea --- /dev/null +++ b/loader/test/proxels/dummy_value_adapter.cpp @@ -0,0 +1,7 @@ +#include "testing/dummy_value_adapter.h" + +namespace flow::test +{ + bool DummyValueAdapter::hasKey(const std::string& key) const + { return key == key_; } +} diff --git a/loader/test/proxels/fauxade.h b/loader/test/proxels/fauxade.h new file mode 100644 index 0000000..fe2a709 --- /dev/null +++ b/loader/test/proxels/fauxade.h @@ -0,0 +1,14 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include + +namespace flow::test +{ +struct Fauxade +{ + using Ptr = std::shared_ptr; + int val = 0; + explicit Fauxade(const int i) : val{i}{} +}; +} diff --git a/loader/test/proxels/include/testing/dummy_value_adapter.h b/loader/test/proxels/include/testing/dummy_value_adapter.h new file mode 100644 index 0000000..004cb11 --- /dev/null +++ b/loader/test/proxels/include/testing/dummy_value_adapter.h @@ -0,0 +1,47 @@ +#pragma once + +#include "superflow/loader/register_factory.h" +#include "boost/preprocessor/stringize.hpp" + +// By using REGISTER_PROXEL_FACTORY_SECTIONED, +// you can register several adapter types at once, and then choose one of the adapters when +// loading the library. +// +// Preferably, a helper macro is wrapping REGISTER_PROXEL_FACTORY_SECTIONED as in this example. +// If the Adapter authors provide similar macros, you could go on like so: +// +// #include "my/json_property_list.h" +// REGISTER_JSON_PROXEL_FACTORY(Dummy, Dummy::static_create) +// +// #include "superflow/yaml/yaml_property_list.h" +// REGISTER_YAML_PROXEL_FACTORY(Dummy, Dummy::static_create) +// +// #include "testing/dummy_value_adapter.h" +// REGISTER_DUMMY_PROXEL_FACTORY(Dummy, Dummy::static_create) + +#define REGISTER_DUMMY_PROXEL_FACTORY(ProxelName, Factory) \ + REGISTER_PROXEL_FACTORY_SECTIONED(ProxelName, Factory, DUMMY_ADAPTER_NAME) + +namespace flow::test +{ +class DummyValueAdapter +{ +public: + template + R convertValue(const std::string&) const + { return R{}; } + + [[nodiscard]] bool hasKey(const std::string& key) const; + + static constexpr const char* adapter_name{BOOST_PP_STRINGIZE(DUMMY_ADAPTER_NAME)}; + + std::string key_{"dummy_key"}; + int int_value{21}; +}; + +template<> +inline int DummyValueAdapter::convertValue(const std::string& key) const +{ + return key == "int key" ? int_value : -1; +} +} diff --git a/loader/test/proxels/lib_path.h.in b/loader/test/proxels/lib_path.h.in new file mode 100644 index 0000000..ad2ba3e --- /dev/null +++ b/loader/test/proxels/lib_path.h.in @@ -0,0 +1,10 @@ +#pragma once +// This file is generated by CMake. Changes will be overwritten. + +#include + +namespace flow::test::local +{ +const std::string lib_path{"$"}; +const std::string lib_dir{"$"}; +} diff --git a/loader/test/proxels/mummy.cpp b/loader/test/proxels/mummy.cpp new file mode 100644 index 0000000..ba14947 --- /dev/null +++ b/loader/test/proxels/mummy.cpp @@ -0,0 +1,36 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/proxel.h" +#include "superflow/loader/register_factory.h" +#include "fauxade.h" + +namespace flow::test +{ +class Mummy : public flow::Proxel +{ +public: + explicit Mummy(int i) + { + setState(State::Paused); + setStatusInfo(std::to_string(i)); + } + + void start() override + { setState(State::Running); } + + void stop() noexcept override + { setState(State::Unavailable); } + + ~Mummy() override = default; + + template + static flow::Proxel::Ptr createWithArgs(const PropertyList&, const Fauxade::Ptr&); +}; + +template +flow::Proxel::Ptr Mummy::createWithArgs(const PropertyList&, const Fauxade::Ptr& f_ptr) +{ + return std::make_shared(f_ptr->val); +} + +REGISTER_PROXEL_FACTORY(Mummy, Mummy::createWithArgs) +} diff --git a/loader/test/proxels/yummy.cpp b/loader/test/proxels/yummy.cpp new file mode 100644 index 0000000..e29907b --- /dev/null +++ b/loader/test/proxels/yummy.cpp @@ -0,0 +1,37 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/proxel.h" +#include "superflow/loader/register_factory.h" +#include "superflow/value.h" + +namespace flow::test +{ +class Yummy : public flow::Proxel +{ +public: + Yummy(int value) + { + setState(State::AwaitingInput); + setStatusInfo(std::to_string(value)); + } + + void start() override + { setState(State::Running); } + + void stop() noexcept override + { setState(State::Unavailable); } + + ~Yummy() override = default; +}; + +namespace +{ +template +flow::Proxel::Ptr freeFunctionCreate(const PropertyList& property_list) +{ + int value = flow::value(property_list, "int key"); + return std::make_shared(value); +} + +REGISTER_PROXEL_FACTORY(Yummy, freeFunctionCreate) +} +} diff --git a/loader/test/test_load_factories.cpp b/loader/test/test_load_factories.cpp new file mode 100644 index 0000000..f4fdad9 --- /dev/null +++ b/loader/test/test_load_factories.cpp @@ -0,0 +1,25 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "testing/dummy_value_adapter.h" + +#include "superflow/loader/load_factories.h" +#include "proxels/lib_path.h" +#include "proxels/fauxade.h" + +#include "gtest/gtest.h" + +using namespace flow; + +TEST(LoadFactories, loadFactories_from_vector_of_libraries) +{ + const std::vector libraries{ + {flow::test::local::lib_dir, "proxels"} + }; + + flow::FactoryMap factories; + + ASSERT_NO_FATAL_FAILURE( + factories = load::loadFactories(libraries) + ); + + ASSERT_FALSE(factories.empty()); +} diff --git a/loader/test/test_proxel_library.cpp b/loader/test/test_proxel_library.cpp new file mode 100644 index 0000000..d1aeeee --- /dev/null +++ b/loader/test/test_proxel_library.cpp @@ -0,0 +1,129 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "testing/dummy_value_adapter.h" + +#include "superflow/loader/proxel_library.h" +#include "proxels/lib_path.h" +#include "proxels/fauxade.h" + +#include "gtest/gtest.h" + +using namespace flow; + +TEST(ProxelLibrary, load_libary_by_library_name) +{ + ASSERT_NO_FATAL_FAILURE( + load::ProxelLibrary(flow::test::local::lib_dir, "proxels") + ); +} + +TEST(ProxelLibrary, load_libary_by_full_path) +{ + ASSERT_NO_FATAL_FAILURE(load::ProxelLibrary(boost::dll::fs::path{flow::test::local::lib_path})); +} + +TEST(ProxelLibrary, nonexisting_path) +{ + ASSERT_THROW( + load::ProxelLibrary(boost::dll::fs::path{flow::test::local::lib_path} / "non-existing path"), + std::invalid_argument + ); +} + +TEST(ProxelLibrary, empty_path) +{ + ASSERT_THROW( + load::ProxelLibrary(boost::dll::fs::path{}), + std::invalid_argument + ); +} + +TEST(ProxelLibrary, loadFactories_from_library) +{ + const load::ProxelLibrary library{flow::test::local::lib_dir, "proxels"}; + + ASSERT_NO_THROW( + std::ignore = library.loadFactories() + ); +} + +TEST(ProxelLibrary, nonexisting_proxel_name) +{ + const load::ProxelLibrary library{flow::test::local::lib_dir, "proxels"}; + const auto factories = library.loadFactories(); + + ASSERT_FALSE(factories.empty()); + + EXPECT_THROW( + factories.get("Non-existing proxel name"), + std::invalid_argument + ); +} + +TEST(ProxelLibrary, can_create_proxel_from_factory) +{ + const load::ProxelLibrary library{flow::test::local::lib_dir, "proxels"}; + const auto factories = library.loadFactories(); + + ASSERT_FALSE(factories.empty()); + + const flow::test::DummyValueAdapter properties; + const auto& dummy_factory = factories.get("Dummy"); + const auto dummy_proxel = std::invoke(dummy_factory, properties); + + EXPECT_EQ(ProxelStatus::State::Paused, dummy_proxel->getStatus().state); +} + +TEST(ProxelLibrary, can_create_proxel_with_value) +{ + const load::ProxelLibrary library{flow::test::local::lib_dir, "proxels"}; + const auto factories = library.loadFactories(); + + ASSERT_FALSE(factories.empty()); + + const flow::test::DummyValueAdapter properties; + const auto yummy_proxel = std::invoke(factories.get("Yummy"), properties); + + EXPECT_EQ(ProxelStatus::State::AwaitingInput, yummy_proxel->getStatus().state); + ASSERT_EQ( + std::to_string(properties.int_value), + yummy_proxel->getStatus().info + ); +} + + +TEST(ProxelLibrary, load_factories_with_args) +{ + const load::ProxelLibrary library{flow::test::local::lib_dir, "proxels"}; + const auto fauxade_ptr = std::make_shared(42); + ASSERT_EQ(42, fauxade_ptr->val); + + const auto factories = library.loadFactories( + fauxade_ptr + ); + + ASSERT_FALSE(factories.empty()); + + const flow::test::DummyValueAdapter properties; + const auto& mummy_factory = factories.get("Mummy"); + const auto mummy_proxel = std::invoke(mummy_factory, properties); + + ASSERT_EQ("42", mummy_proxel->getStatus().info); +} + +TEST(ProxelLibrary, ProxelWorks) +{ + const load::ProxelLibrary library{flow::test::local::lib_dir, "proxels"}; + const auto factories = library.loadFactories(); + + ASSERT_FALSE(factories.empty()); + + const flow::test::DummyValueAdapter properties; + const auto& dummy_factory = factories.get("Dummy"); + const auto dummy_proxel = std::invoke(dummy_factory, properties); + + EXPECT_EQ(ProxelStatus::State::Paused, dummy_proxel->getStatus().state); + dummy_proxel->start(); + EXPECT_EQ(ProxelStatus::State::Running, dummy_proxel->getStatus().state); + dummy_proxel->stop(); + EXPECT_EQ(ProxelStatus::State::Unavailable, dummy_proxel->getStatus().state); +} diff --git a/loader/test/test_rtld_now.cpp b/loader/test/test_rtld_now.cpp new file mode 100644 index 0000000..6e5a8d4 --- /dev/null +++ b/loader/test/test_rtld_now.cpp @@ -0,0 +1,29 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "testing/dummy_value_adapter.h" + +#include "superflow/loader/proxel_library.h" +#include "proxels/lib_path.h" +#include "proxels/fauxade.h" + +#include "gtest/gtest.h" + +using namespace flow; + +TEST(ProxelLibrary, rtld_now_missing_dependency) +{ + /// libdependent_library.so depends on libmissing_dependency.so, but libmissing_dependency.so is not on the linker path. + /// rtld_now should cause the loading of libdependent_library.so to fail. + EXPECT_THROW( + std::ignore = load::ProxelLibrary(flow::test::local::lib_dir, "dependent_library"), + std::invalid_argument + ); +} + +TEST(ProxelLibrary, rtld_now_dependencies_resolved) +{ + /// libmissing_dependency.so has no dependencies that are not on the default linker path. + /// It should thus load just fine. + EXPECT_NO_THROW( + std::ignore = load::ProxelLibrary(flow::test::local::lib_dir, "missing_dependency") + ); +} diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt new file mode 100644 index 0000000..91a844e --- /dev/null +++ b/test_package/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.15) +project(PackageTest CXX) + +set(name superflow) +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_BINARY_DIR}) +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + +enable_testing() +find_package(${name} REQUIRED core curses loader yaml) + +include(GenerateTestFromModule) + +set(modules core curses loader yaml) +foreach(module ${modules}) + generate_test_from_module(${name} ${module}) +endforeach() + +if(TARGET superflow::loader) + target_link_libraries(superflow_loader_runtime_test superflow::yaml) +endif() diff --git a/test_package/GenerateTestFromModule.cmake b/test_package/GenerateTestFromModule.cmake new file mode 100644 index 0000000..6d0a276 --- /dev/null +++ b/test_package/GenerateTestFromModule.cmake @@ -0,0 +1,16 @@ +macro(generate_test_from_module namespace module_name) + if(TARGET ${namespace}::${module_name}) + message(STATUS "generate_test_from_module(${namespace} ${module_name})") + add_executable(${namespace}_${module_name}_runtime_test src/${namespace}${module_name}_runtime_test.cpp) + target_link_libraries(${namespace}_${module_name}_runtime_test ${namespace}::${module_name}) + set_target_properties(${namespace}_${module_name}_runtime_test PROPERTIES + CXX_STANDARD_REQUIRED ON + CXX_STANDARD 17 + BUILD_RPATH $ORIGIN + INSTALL_RPATH $ORIGIN + ) + add_test(${namespace}_${module_name}_runtime_test ${namespace}_${module_name}_runtime_test) + else() + message(WARNING "generate_test_from_module: nonexisting target ${namespace}::${module_name}") + endif() +endmacro() diff --git a/test_package/conanfile.py b/test_package/conanfile.py new file mode 100644 index 0000000..bb92de5 --- /dev/null +++ b/test_package/conanfile.py @@ -0,0 +1,28 @@ +import os + +from conan import ConanFile +from conan.tools.cmake import CMake, cmake_layout +from conan.tools.build import can_run + + +class superflowTestConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeDeps", "CMakeToolchain" + + def requirements(self): + self.requires(self.tested_reference_str) + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def layout(self): + cmake_layout(self) + + def test(self): + if can_run(self): + cmake = CMake(self) + cmake.test(env="conanrun") + #cmd = os.path.join(self.cpp.build.bindir, "example") + #self.run(cmd, env="conanrun") diff --git a/test_package/src/superflowcore_runtime_test.cpp b/test_package/src/superflowcore_runtime_test.cpp new file mode 100644 index 0000000..fda3eac --- /dev/null +++ b/test_package/src/superflowcore_runtime_test.cpp @@ -0,0 +1,7 @@ +#include "superflow/utils/metronome.h" +#include + +int main() +{ + flow::Metronome metronome{[](const auto) {}, std::chrono::microseconds{1}}; +} diff --git a/test_package/src/superflowcurses_runtime_test.cpp b/test_package/src/superflowcurses_runtime_test.cpp new file mode 100644 index 0000000..619f1c6 --- /dev/null +++ b/test_package/src/superflowcurses_runtime_test.cpp @@ -0,0 +1,6 @@ +#include "superflow/curses/graph_gui.h" + +int main() +{ + flow::curses::GraphGUI gui{}; +} diff --git a/test_package/src/superflowloader_runtime_test.cpp b/test_package/src/superflowloader_runtime_test.cpp new file mode 100644 index 0000000..0d37468 --- /dev/null +++ b/test_package/src/superflowloader_runtime_test.cpp @@ -0,0 +1,13 @@ +#include "superflow/loader/load_factories.h" +#include "superflow/yaml/yaml_property_list.h" + +int main() +{ + try + { + const std::vector libraries{{"path to library"}}; + + const auto factory_map = flow::load::loadFactories(libraries); + } catch (...) + {} +} diff --git a/test_package/src/superflowyaml_runtime_test.cpp b/test_package/src/superflowyaml_runtime_test.cpp new file mode 100644 index 0000000..6b2ba9b --- /dev/null +++ b/test_package/src/superflowyaml_runtime_test.cpp @@ -0,0 +1,11 @@ +#include "superflow/graph.h" +#include "superflow/yaml/yaml.h" + +int main() +{ + try + { + const auto graph = flow::yaml::createGraph("", flow::yaml::FactoryMap{}); + } catch (...) + {} +} diff --git a/yaml/CMakeLists.txt b/yaml/CMakeLists.txt new file mode 100644 index 0000000..83a4cc8 --- /dev/null +++ b/yaml/CMakeLists.txt @@ -0,0 +1,26 @@ +project(yaml) +init_module() + +find_package(yaml-cpp REQUIRED) + +add_library_boilerplate() + +option(SET_LOADER_ADAPTER "Set the LOADER_ADAPTER_xxx macros. Turn off if you are linking more than one adapter." ON) +if (SET_LOADER_ADAPTER) + target_compile_definitions(${target_name} + INTERFACE + LOADER_ADAPTER_HEADER="superflow/yaml/yaml_property_list.h" + LOADER_ADAPTER_NAME=YAML + LOADER_ADAPTER_TYPE=flow::yaml::YAMLPropertyList + ) +endif () + +target_link_libraries(${target_name} + PUBLIC + superflow::core + yaml-cpp::yaml-cpp + ) + +if (BUILD_TESTS) + add_subdirectory(test) +endif() diff --git a/yaml/include/superflow/yaml/factory.h b/yaml/include/superflow/yaml/factory.h new file mode 100644 index 0000000..adb98f1 --- /dev/null +++ b/yaml/include/superflow/yaml/factory.h @@ -0,0 +1,14 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/yaml/yaml_property_list.h" + +#include "superflow/factory_map.h" +#include "superflow/proxel_config.h" + +namespace flow::yaml +{ +using Factory = flow::Factory; +using FactoryMap = flow::FactoryMap; +using ProxelConfig = flow::ProxelConfig; +} diff --git a/yaml/include/superflow/yaml/yaml.h b/yaml/include/superflow/yaml/yaml.h new file mode 100644 index 0000000..0cde235 --- /dev/null +++ b/yaml/include/superflow/yaml/yaml.h @@ -0,0 +1,112 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "superflow/yaml/factory.h" + +#include "yaml-cpp/yaml.h" + +#include +#include + +namespace flow { class Graph; } + +namespace flow::yaml +{ +/// Identifies a section in the YAML file. +/// This is a "path" because it can be nested in the Yaml file, e.g. +/// {"Toplevel", "Sublevel", "SubSubLevel", "Proxels"} +using SectionPath = std::vector; + +/// Sections in the YAML file to look for proxels. +const std::vector default_proxel_section_paths = {{"Proxels"}}; + +/// \brief Create a flow::Graph from the given YAML config file. +/// +/// The structure and contents of the configuration file must follow a distinct pattern. +/// Two lists are required: \b Proxels and \b Connections. +/// In the `Proxels` list, an entry consists of a unique name, followed by configuration parameters for that proxel. +/// The only required parameter is `type`, which is used as a key to the FactoryMap, i.e. to retrieve the correct Factory. +/// The `Connections` list defines how specific ports on specific proxels are connected together. +/// +/// An additional feature included in version 1.5 is the option to include other config files +/// using the \b Includes list. All included files must obey the pattern of a config file, but proxels and connection +/// specifications can be scattered across the union of the files. +/// Only the first file (the one given as argument to this function) is parsed for includes. +/// +/// The listing below is a valid example of a configuration file for the yaml module. +/// For further documentation, see the superflow report 19/00776. +/// \code{.yml} +/// %YAML 1.2 +/// --- +/// Includes: +/// - sunny_day.yaml +/// - rainy_day.yaml +/// +/// Proxels: +/// proxel_1: # unique name of the proxel +/// type : "MyProxel" # class name +/// my_param : 42 # proxel-specific parameter +/// +/// proxel_2: # unique name of the proxel +/// type : "YourProxel" # class name +/// enable : false # leave this proxel out of the graph +/// +/// Connections: +/// - [proxel_1: 'out', proxel_2: 'in'] +/// ... +/// \endcode +/// \param config_file_path Absolute path to the YAML configuration file +/// \param factory_map Container for mapping Proxel types to their respective Factories +/// \param proxel_section_paths Where to find proxels in the config file. Default is a top level "Proxels" section. +/// \param config_search_directory If the config file utilizes the 'Includes' feature, +/// search for included files relative to this directory. +/// \return A new graph, ready to be started +/// \throws std::runtime_error If the file(s) cannot be found or if something is wrong with the syntax +flow::Graph createGraph( + const std::string& config_file_path, + const FactoryMap& factory_map, + const std::vector& proxel_section_paths = default_proxel_section_paths, + const std::string& config_search_directory = {} +); + +flow::Graph createGraph( + const YAML::Node& root, + const FactoryMap& factory_map, + const std::vector& proxel_section_paths = default_proxel_section_paths, + const std::string& config_search_directory = {} +); + +/// \brief Returns a list with the ID of all proxel configs with `flag` set to true. +/// Useful for creating subsets of proxels that require special treatment. For example: +/// \code{.yml} +/// Proxels: +/// my_proxel_1: +/// type: MyProxel +/// my-flag: true +/// +/// my_proxel_2: +/// type: MyProxel +/// my-flag: false +/// \endcode +/// `getFlaggedProxels(..., "my-flag")` will then return a list containing only "my_proxel_1". +/// \param root The YAML::Node to parse for flagged proxels +/// \param flag The key to look for, e.g. "my-flag" +/// \param proxel_section_paths Sections in the YAML::Node to look for proxels. +/// \return List of proxels with the `flag` enabled +[[nodiscard]] std::vector getFlaggedProxels( + const YAML::Node& root, + const std::string& flag, + const std::vector& proxel_section_paths = default_proxel_section_paths +); + +std::vector extractLibPaths( + const std::string& config_path, + const std::string& section_name = "LibraryPaths" +); + +std::string generateDOTFile( + const std::string& config_file_path, + const std::vector& proxel_section_paths = default_proxel_section_paths, + const std::string& config_search_directory = {} +); +} diff --git a/yaml/include/superflow/yaml/yaml_property_list.h b/yaml/include/superflow/yaml/yaml_property_list.h new file mode 100644 index 0000000..37411be --- /dev/null +++ b/yaml/include/superflow/yaml/yaml_property_list.h @@ -0,0 +1,65 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "yaml-cpp/yaml.h" + +#include + +namespace flow::yaml +{ +class YAMLPropertyList +{ +public: + YAMLPropertyList() = default; + + explicit YAMLPropertyList(const YAML::Node& parent); + + bool hasKey(const std::string& key) const; + + template + T convertValue(const std::string& key) const; + + static constexpr const char* adapter_name{"YAML"}; + +private: + YAML::Node parent_; +}; + +// ----- Implementation ----- +inline YAMLPropertyList::YAMLPropertyList(const YAML::Node& parent) + : parent_{parent} +{ + if (!parent.IsMap()) + { + throw std::invalid_argument("Input node to YAMLPropertyList must be a YAML::Map."); + } +} + +inline bool YAMLPropertyList::hasKey(const std::string& key) const +{ + return bool(parent_[key]); +} + +template +T YAMLPropertyList::convertValue(const std::string& key) const +{ + const auto& node = parent_[key]; + if (!node) + { + throw std::runtime_error({"Could not find key \"" + key + "\" in property list"}); + } + + try + { + return node.as(); + } + catch (const YAML::TypedBadConversion&) + { + throw std::runtime_error({"Type mismatch for key: \"" + key + "\""}); + } + catch (const YAML::BadConversion&) + { + throw std::runtime_error({"Failed to parse key: \"" + key + "\""}); + } +} +} diff --git a/yaml/include/superflow/yaml/yaml_string_pair.h b/yaml/include/superflow/yaml/yaml_string_pair.h new file mode 100644 index 0000000..3c652ea --- /dev/null +++ b/yaml/include/superflow/yaml/yaml_string_pair.h @@ -0,0 +1,65 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#pragma once + +#include "yaml-cpp/node/convert.h" + +#include +#include + +namespace YAML +{ +template<> +struct convert> +{ + static Node encode(const std::pair& rhs) + { + Node node(NodeType::Sequence); + node.push_back(rhs.first); + node.push_back(rhs.second); + return node; + } + + static bool decode(const Node& node, std::pair& rhs) + { + if (node.IsMap() && node.size() == 1) + { + rhs.first = node.begin()->first.as(); + rhs.second = node.begin()->second.as(); + return true; + } + + return false; + } +}; + +template<> +struct convert>> +{ + static Node encode(const std::pair>& pair) + { + Node node(NodeType::Sequence); + + node.push_back(pair.first); + node.push_back(pair.second); + + return node; + } + + static bool decode(const Node& node, std::pair>& rhs) + { + if (node.IsMap() && node.size() == 1) + { + rhs.first = node.begin()->first.as(); + + const auto& second = node.begin()->second; + rhs.second = second.IsSequence() + ? second.as>() + : std::vector{second.as()}; + + return true; + } + + return false; + } +}; +} diff --git a/yaml/src/yaml.cpp b/yaml/src/yaml.cpp new file mode 100644 index 0000000..e5a421d --- /dev/null +++ b/yaml/src/yaml.cpp @@ -0,0 +1,688 @@ +// Copyright 2019, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/yaml/yaml.h" +#include "superflow/yaml/yaml_string_pair.h" + +#include "superflow/connection_spec.h" +#include "superflow/graph.h" +#include "superflow/graph_factory.h" +#include "superflow/utils/graphviz.h" +#include "superflow/value.h" + +#include +#include +#include + +namespace fs = std::filesystem; +namespace +{ +using namespace flow::yaml; + +using ReplicaMap = std::map; +using PortSpecification = std::pair>; +using ExpandedPortSpecification = std::vector>; + +ExpandedPortSpecification expandConnectionSpecifier(const PortSpecification& port_spec, size_t proxel_replicas); +std::vector getAllProxelConfigs(const std::vector& config_sections); +std::vector getProxelConfigs(const YAML::Node& section); +ProxelConfig createProxelConfig(const std::string& unique_id, const YAML::Node& properties); +std::vector getAllProxelNamesFilteredByEnableValue(const std::vector& config_sections, bool enable_value); +std::vector getProxelNamesFilteredByEnableValue(const YAML::Node& section, bool enable_value); +std::vector getReplicatedConfigs(const std::string& unique_id, size_t num_replicas, const YAML::Node& properties); +ReplicaMap getAllReplicatedProxels(const std::vector& config_sections); +std::vector getConnections( + const YAML::Node& config, + const std::vector& enabled_proxels, + const ReplicaMap& replica_map); + +bool validConnectionSpecification(const YAML::Node& node); +bool validPortSpecification(const YAML::Node& node); + +ReplicaMap getReplicatedProxels(const YAML::Node& section); +size_t getNumberOfProxelReplicas(const YAML::Node& proxel_config); +std::string getProxelReplicaId(const std::string& name, size_t idx); + +std::vector getProxelSections( + const YAML::Node& root, + const std::vector& proxel_section_paths); +std::vector getAllProxelSections( + const std::vector&, + const std::vector& proxel_section_paths +); + + +std::vector openAllConfigFiles( + const std::string& config_file_path, + const std::string& config_search_directory = {} +); + +std::vector getAllConnections( + const std::vector& config_roots, + const std::vector& proxel_section_paths +); + +std::vector getUnconnectedProxels( + const std::vector& enabled_proxels, + const std::vector& all_connections +); + +template +std::vector operator+(const std::vector& lhs, const std::vector& rhs); + +template +void operator+=(std::vector& lhs, const std::vector& rhs); + +template +std::map operator+(const std::map& lhs, const std::map& rhs); + +std::ostream& operator<<(std::ostream& stream, const SectionPath& path); +} + +namespace flow::yaml +{ +flow::Graph createGraph( + const std::string& config_file_path, + const FactoryMap& factory_map, + const std::vector& proxel_section_paths, + const std::string& config_search_directory +) +{ + const auto root = YAML::LoadFile(config_file_path); + if (config_search_directory.empty()) + { + const auto parent_path = fs::path{config_file_path}.parent_path().string(); + return createGraph(root, factory_map, proxel_section_paths, parent_path); + } + + return createGraph(root, factory_map, proxel_section_paths, config_search_directory); +} + +flow::Graph createGraph( + const YAML::Node& root, + const FactoryMap& factory_map, + const std::vector& proxel_section_paths, + const std::string& config_search_directory +) +{ + YAML::Node node = YAML::Clone(root); + auto all_config_sections = getProxelSections(node, proxel_section_paths); + + if (node["Includes"]) + { + for (const auto& included_file : node["Includes"]) + { + const auto filename = included_file.as(); + YAML::Node incl; + try + { incl = YAML::LoadFile(filename); } + catch (const YAML::BadFile&) + { + const auto full_path = fs::path{config_search_directory} /= filename; + incl = YAML::LoadFile(full_path.string()); + } + + if (!incl["Connections"]) + { throw std::invalid_argument("No section 'Connections' specified in file: " + filename); } + + for (const auto& connection : incl["Connections"]) + { node["Connections"].push_back(connection); } + + all_config_sections = all_config_sections + getProxelSections(incl, proxel_section_paths); + } + } + + const auto proxel_configurations = getAllProxelConfigs(all_config_sections); + const auto enabled_proxels = getAllProxelNamesFilteredByEnableValue(all_config_sections, true); + const auto replicated_proxels = getAllReplicatedProxels(all_config_sections); + const auto connections = getConnections(node, enabled_proxels, replicated_proxels); + + return flow::createGraph(factory_map, proxel_configurations, connections); +} + +std::vector getFlaggedProxels( + const YAML::Node& root, + const std::string& flag, + const std::vector& proxel_section_paths +) +{ + const auto config_sections = getProxelSections(root, proxel_section_paths); + const auto proxel_configurations = getAllProxelConfigs(config_sections); + + std::vector flagged_ids; + + for (const auto& config : proxel_configurations) + { + if (!config.properties.hasKey(flag)) + { + continue; + } + + if (value(config.properties, flag)) + { + flagged_ids.push_back(config.id); + } + } + + return flagged_ids; +} + +std::vector extractLibPaths( + const std::string& config_path, + const std::string& section_name +) +{ + const auto config = YAML::LoadFile(config_path); + + if (!config[section_name]) + { throw std::invalid_argument("No " + section_name + " section defined in config."); } + + std::vector lib_paths; + for (const auto& lib_path : config[section_name]) + { lib_paths.push_back(lib_path.as()); } + + return lib_paths; +} + +std::string generateDOTFile( + const std::string& config_file_path, + const std::vector& proxel_section_paths, + const std::string& config_search_directory +) +{ + const auto config_roots = openAllConfigFiles( + config_file_path, + config_search_directory + ); + const auto all_connections = getAllConnections( + config_roots, + proxel_section_paths + ); + + const auto all_proxel_sections = getAllProxelSections(config_roots, proxel_section_paths); + const auto enabled_proxels = getAllProxelNamesFilteredByEnableValue(all_proxel_sections, true); + const auto unconnected_proxels = getUnconnectedProxels(enabled_proxels, all_connections); + + std::vector dummy_connections; + for (const auto& proxel_name : unconnected_proxels) + { + dummy_connections.push_back({proxel_name,{},{},{}}); + } + + return flow::GraphViz{ + all_connections + dummy_connections + }.employ(); +} +} + +// ----- Helper functions ----- +namespace +{ +std::vector getAllProxelConfigs(const std::vector& config_sections) +{ + std::vector configs; + + for (const YAML::Node& section : config_sections) + { + configs = configs + getProxelConfigs(section); + } + + return configs; +} + +std::vector getAllProxelNamesFilteredByEnableValue(const std::vector& config_sections, bool enable_value) +{ + std::vector proxel_names; + + for (const YAML::Node& section : config_sections) + { + proxel_names = proxel_names + getProxelNamesFilteredByEnableValue(section, enable_value); + } + + return proxel_names; +} + +ReplicaMap getAllReplicatedProxels(const std::vector& config_sections) +{ + ReplicaMap replicated_proxels; + + for (const YAML::Node& section : config_sections) + { + replicated_proxels = replicated_proxels + getReplicatedProxels(section); + } + + return replicated_proxels; +} + +ProxelConfig createProxelConfig(const std::string& unique_id, const YAML::Node& properties) +{ + const auto type = properties["type"].as(); + return { + unique_id, + type, + YAMLPropertyList{properties} + }; +} + +std::vector getProxelConfigs(const YAML::Node& section) +{ + std::vector configs; + + for (const auto& p : section) + { + const auto enable = (p.second["enable"]) ? p.second["enable"].as() : true; + + if (!enable) + { continue; } + + const auto unique_id = p.first.as(); + const YAML::Node& properties = p.second; + + const size_t num_replicas = getNumberOfProxelReplicas(properties); + + if (num_replicas > 1) + { + configs = configs + getReplicatedConfigs(unique_id, num_replicas, properties); + } + else + { + configs.push_back(createProxelConfig(unique_id, properties)); + } + } + + return configs; +} + +std::vector getReplicatedConfigs(const std::string& unique_id, const size_t num_replicas, const YAML::Node& properties) +{ + std::vector configs; + + for (size_t idx = 0; idx < num_replicas; ++idx) + { + YAML::Node replica_props = YAML::Clone(properties); + replica_props.remove("replicate"); + + for(const auto& kv : properties) + { + const auto key = kv.first.as(); + const auto value = kv.second; + + if (key.front() == '$') + { + if (value.size() != num_replicas) + { + throw std::invalid_argument( + {key + ": properties with '$' requires a list of properties with size equal to number of replicas."} + ); + } + replica_props.remove(key); + replica_props[key.substr(1)] = value[idx]; + } + } + + const std::string proxel_id = getProxelReplicaId(unique_id, idx); + configs.push_back(createProxelConfig(proxel_id, replica_props)); + } + + return configs; +} + +std::vector getConnections( + const YAML::Node& config, + const std::vector& enabled_proxels, + const ReplicaMap& replica_map) +{ + if (!config["Connections"]) + { throw std::invalid_argument("No section 'Connections' specified in config."); } + + const auto is_enabled = [&enabled_proxels](const std::string& name) + { + const auto it = std::find(enabled_proxels.begin(), enabled_proxels.end(), name); + return it != enabled_proxels.end(); + }; + + std::vector connections; + connections.reserve(config["Connections"].size()); + + for (const auto& connection_pair : config["Connections"]) + { + if (!validConnectionSpecification(connection_pair)) + { + throw std::invalid_argument("Bad connection format. Connection must be a YAML::Sequence of two YAML::Maps.\n" + " E.g.: [proxel1: port, proxel2: port]\n" + " or : [proxel1: [port1, port2], replicated_proxel: port]" + ); + } + + const auto lhs = connection_pair[0].as(); + const auto rhs = connection_pair[1].as(); + + const std::string& lhs_base_id = lhs.first; + const std::string& rhs_base_id = rhs.first; + + if (!is_enabled(lhs_base_id) || !is_enabled(rhs_base_id)) + { continue; } + + const auto lhs_replicas = (replica_map.count(lhs_base_id) > 0) ? replica_map.at(lhs_base_id) : 1; + const auto rhs_replicas = (replica_map.count(rhs_base_id) > 0) ? replica_map.at(rhs_base_id) : 1; + + auto lhs_list = expandConnectionSpecifier(lhs, lhs_replicas); + auto rhs_list = expandConnectionSpecifier(rhs, rhs_replicas); + + { + const auto lhs_size = lhs_list.size(); + const auto rhs_size = rhs_list.size(); + + if (lhs_size != rhs_size && lhs_size != 1 && rhs_size != 1) + { + std::ostringstream ss; + ss << "Attempted connecting " << lhs_size + << " ports on " << lhs_base_id + << " to " << rhs_size << " ports on " + << rhs_base_id; + + throw std::invalid_argument(ss.str()); + } + + const auto size = std::max(lhs_size, rhs_size); + + if (lhs_size != size) + { + lhs_list.resize(size, lhs_list.front()); + } + + if (rhs_size != size) + { + rhs_list.resize(size, rhs_list.front()); + } + } + + for (size_t i = 0; i < lhs_list.size(); ++i) + { + connections.push_back( + { + lhs_list[i].first, lhs_list[i].second, + rhs_list[i].first, rhs_list[i].second + }); + } + } + + return connections; +} + +ExpandedPortSpecification expandConnectionSpecifier(const PortSpecification& port_spec, const size_t proxel_replicas) +{ + const auto& proxel_id = port_spec.first; + const auto& port_list = port_spec.second; + + if ((proxel_replicas > 1 && port_list.size() > 1)) + { + throw std::invalid_argument( + "Ambigous port specification using port list [P: [a,b]] for replicated proxel. Use e.g." + "\n - [P: a, P2: ...] " + "\n - [P: b, P2: ...]" + ); + } + + ExpandedPortSpecification result; + + if (proxel_replicas > 1) + { + for (size_t idx = 0; idx < proxel_replicas; ++idx) + { + const auto replica_id = getProxelReplicaId(proxel_id, idx); + result.emplace_back(replica_id, port_list[0]); + } + } + else if (port_list.size() > 1) + { + for (const auto& port : port_list) + { result.emplace_back(proxel_id, port); } + } + else + { + result.emplace_back(proxel_id, port_list[0]); + } + + return result; +} + +std::vector getProxelNamesFilteredByEnableValue(const YAML::Node& section, const bool enable_value) +{ + std::vector proxel_names; + + for (const auto& p : section) + { + const auto is_enabled = (p.second["enable"]) ? p.second["enable"].as() : true; + + if (is_enabled == enable_value) + { + const size_t num_replicas = getNumberOfProxelReplicas(p.second); + const auto unique_id = p.first.as(); + + if (num_replicas > 1) + { + for (size_t idx = 0; idx < num_replicas; ++idx) + { + const std::string proxel_id = getProxelReplicaId(unique_id, idx); + proxel_names.emplace_back(proxel_id); + } + } + + proxel_names.emplace_back(unique_id); + } + } + + return proxel_names; +} + +size_t getNumberOfProxelReplicas(const YAML::Node& proxel_config) +{ + return proxel_config["replicate"] + ? proxel_config["replicate"].as() + : 1; +} + +std::string getProxelReplicaId( + const std::string& name, + const size_t idx) +{ + return name + "_" + std::to_string(idx); +} + +ReplicaMap getReplicatedProxels(const YAML::Node& section) +{ + ReplicaMap names; + + for (const auto& p : section) + { + const size_t num_replicas = getNumberOfProxelReplicas(p.second); + + if (num_replicas > 1) + { + const auto unique_id = p.first.as(); + + names[unique_id] = num_replicas; + } + } + + return names; +} + +template +std::vector operator+(const std::vector& lhs, const std::vector& rhs) +{ + std::vector sum{lhs}; + sum.reserve(lhs.size() + rhs.size()); + + sum.insert(sum.end(), rhs.begin(), rhs.end()); + + return sum; +} + +template +void operator+=(std::vector& lhs, const std::vector& rhs) +{ + lhs.insert(lhs.end(), rhs.begin(), rhs.end()); +} + +template +std::map operator+(const std::map& lhs, const std::map& rhs) +{ + std::map sum{lhs}; + sum.insert(rhs.begin(), rhs.end()); + return sum; +} + +std::ostream& operator<<(std::ostream& stream, const SectionPath& path) +{ + if (!path.empty()) + { + stream << path.front(); + } + + for (size_t i = 1; i < path.size(); ++i) + { + stream << "/" << path[i]; + } + + return stream; +} + +std::vector getProxelSections( + const YAML::Node& root, + const std::vector& proxel_section_paths) +{ + std::vector sections; + sections.reserve(proxel_section_paths.size()); + + for (const SectionPath& proxel_section_path : proxel_section_paths) + { + YAML::Node node = root; + + for (const std::string& step_name : proxel_section_path) + { + node.reset(node[step_name]); + + if (!node) + { + std::ostringstream ss; + ss << "no proxel section '" << proxel_section_path << "' defined in config"; + + throw std::invalid_argument(ss.str()); + } + } + + sections.push_back(node); + } + + return sections; +} + +std::vector +getAllProxelSections( + const std::vector& config_roots, + const std::vector& proxel_section_paths +) +{ + std::vector all_proxel_sections; + for (const auto& node : config_roots) + { + all_proxel_sections += getProxelSections(node, proxel_section_paths); + } + return all_proxel_sections; +} + +std::vector openAllConfigFiles( + const std::string& config_file_path, + const std::string& config_search_directory +) +{ + std::vector config_files; + const auto include_directory = !config_search_directory.empty() + ? fs::path{config_search_directory} + : fs::path{config_file_path}.parent_path(); + + config_files.push_back(YAML::LoadFile(config_file_path)); + const auto& root = config_files.front(); + + if (!root["Connections"]) + { throw std::invalid_argument("No section 'Connections' specified in file: " + config_file_path); } + + if (root["Includes"]) + { + for (const auto& include_entry : root["Includes"]) + { + const auto filename = include_entry.as(); + const auto full_path = fs::is_regular_file(filename) ? filename : (include_directory / filename).string(); + config_files.push_back(YAML::LoadFile(full_path)); + + const auto& node = config_files.back(); + if (!node["Connections"]) + { throw std::invalid_argument("No section 'Connections' specified in file: " + full_path); } + } + } + return config_files; +} + +std::vector getAllConnections( + const std::vector& config_roots, + const std::vector& proxel_section_paths +) +{ + YAML::Node connections; + + for (const auto& node : config_roots) + { + for (const auto& connection : node["Connections"]) + { connections["Connections"].push_back(connection); } + } + + const auto all_proxel_sections = getAllProxelSections(config_roots, proxel_section_paths); + const auto enabled_proxels = getAllProxelNamesFilteredByEnableValue(all_proxel_sections, true); + const auto replicated_proxels = getAllReplicatedProxels(all_proxel_sections); + auto connection_specs = getConnections(connections, enabled_proxels, replicated_proxels); + + return connection_specs; +} + +std::vector getUnconnectedProxels( + const std::vector& enabled_proxels, + const std::vector& all_connections +) +{ + std::set connected_proxels; + for (const auto& connection: all_connections) + { + connected_proxels.insert(connection.lhs_name); + connected_proxels.insert(connection.rhs_name); + } + + std::vector unconnected_proxels; + + for (const auto& proxel_name : enabled_proxels) + { + const auto it = std::find(connected_proxels.begin(), connected_proxels.end(), proxel_name); + if (it == connected_proxels.end()) + { + unconnected_proxels.push_back(proxel_name); + } + } + + return unconnected_proxels; +} + +bool validConnectionSpecification(const YAML::Node& node) +{ + return node.IsSequence() + && node.size() == 2 + && validPortSpecification(node[0]) + && validPortSpecification(node[1]); +} + +bool validPortSpecification(const YAML::Node& node) +{ + return node.IsMap() + && node.size() == 1 + && (node.begin()->second.IsScalar() || node.begin()->second.IsSequence()); +} +} + diff --git a/yaml/test/CMakeLists.txt b/yaml/test/CMakeLists.txt new file mode 100644 index 0000000..91b09a4 --- /dev/null +++ b/yaml/test/CMakeLists.txt @@ -0,0 +1,25 @@ +set(PARENT_PROJECT ${PROJECT_NAME}) +project(${CMAKE_PROJECT_NAME}-${PARENT_PROJECT}-test CXX) + +message(STATUS "* Adding test executable '${PROJECT_NAME}'") +add_executable(${PROJECT_NAME} + "yaml-test-proxel.cpp" + "test_yaml_property_list.cpp" +) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE + GTest::gtest GTest::gtest_main + ${CMAKE_PROJECT_NAME}::loader + ${CMAKE_PROJECT_NAME}::yaml +) + +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD_REQUIRED ON + CXX_STANDARD 17 + ENABLE_EXPORTS ON +) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}) diff --git a/yaml/test/test_yaml_property_list.cpp b/yaml/test/test_yaml_property_list.cpp new file mode 100644 index 0000000..1db2176 --- /dev/null +++ b/yaml/test/test_yaml_property_list.cpp @@ -0,0 +1,49 @@ +// Copyright 2023, Forsvarets forskningsinstitutt. All rights reserved. +#include "gtest/gtest.h" +#include "superflow/loader/proxel_library.h" +#include "superflow/yaml/yaml_property_list.h" + +#include "boost/dll/runtime_symbol_info.hpp" // for program_location() +#include + + +namespace dll = boost::dll; + +TEST(SuperflowYaml, can_load_load_test_libary) +{ + ASSERT_NO_FATAL_FAILURE(flow::load::ProxelLibrary library{dll::program_location()}); +} + +TEST(SuperflowYaml, can_load_adapter_name) +{ + const flow::load::ProxelLibrary library{dll::program_location()}; + + ASSERT_NO_THROW( + std::ignore = library.loadFactories() + ); +} + +TEST(SuperflowYaml, can_load_factories_and_create_proxel) +{ + const flow::load::ProxelLibrary library{dll::program_location()}; + + const auto factories = library.loadFactories(); + + ASSERT_FALSE(factories.empty()); + + const auto& proxel_factory = factories.get("YamlTestProxel"); + + constexpr int the_number{42}; + const auto load_yaml_properties = [the_number] + { + YAML::Node node; + node["int"] = the_number; + return flow::yaml::YAMLPropertyList(node); + }; + + const auto properties = load_yaml_properties(); + const auto proxel = std::invoke(proxel_factory, properties); + + EXPECT_EQ(flow::ProxelStatus::State::AwaitingInput, proxel->getStatus().state); + ASSERT_EQ(std::to_string(the_number), proxel->getStatus().info); +} diff --git a/yaml/test/yaml-test-proxel.cpp b/yaml/test/yaml-test-proxel.cpp new file mode 100644 index 0000000..c83f4d0 --- /dev/null +++ b/yaml/test/yaml-test-proxel.cpp @@ -0,0 +1,37 @@ +// Copyright 2023, Forsvarets forskningsinstitutt. All rights reserved. +#include "superflow/loader/register_factory.h" +#include "superflow/proxel.h" +#include "superflow/value.h" + +namespace flow::test +{ +class YamlTestProxel : public flow::Proxel +{ +public: + YamlTestProxel(int value) + { + setState(State::AwaitingInput); + setStatusInfo(std::to_string(value)); + } + + void start() override + { setState(State::Running); } + + void stop() noexcept override + { setState(State::Unavailable); } + + ~YamlTestProxel() override = default; +}; + +namespace +{ +template +flow::Proxel::Ptr createYamlTestProxel(const PropertyList& property_list) +{ + int value = flow::value(property_list, "int"); + return std::make_shared(value); +} + +REGISTER_PROXEL_FACTORY(YamlTestProxel, createYamlTestProxel) +} +} diff --git a/yaml/yaml-config.cmake.in b/yaml/yaml-config.cmake.in new file mode 100644 index 0000000..ea39f93 --- /dev/null +++ b/yaml/yaml-config.cmake.in @@ -0,0 +1,9 @@ +@PACKAGE_INIT@ +message(STATUS "* Found @CMAKE_PROJECT_NAME@::@PROJECT_NAME@: " "${CMAKE_CURRENT_LIST_FILE}") +include(CMakeFindDependencyMacro) +find_dependency(yaml-cpp) + +set(@CMAKE_PROJECT_NAME@_@PROJECT_NAME@_FOUND TRUE) + +check_required_components(@PROJECT_NAME@) +message(STATUS "* Loading @CMAKE_PROJECT_NAME@::@PROJECT_NAME@ complete")