diff --git a/.codespell_ignore_words b/.codespell_ignore_words new file mode 100644 index 000000000..ab09b3c2f --- /dev/null +++ b/.codespell_ignore_words @@ -0,0 +1,7 @@ +INOUT +InOut +delimeter +Succesful +worl +valu +Exeption diff --git a/.github/workflows/cmake_ubuntu.yml b/.github/workflows/cmake_ubuntu.yml index aaf5f628d..41ed9a196 100644 --- a/.github/workflows/cmake_ubuntu.yml +++ b/.github/workflows/cmake_ubuntu.yml @@ -1,6 +1,11 @@ name: cmake Ubuntu -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) @@ -15,7 +20,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-20.04] + os: [ubuntu-22.04] steps: - uses: actions/checkout@v2 @@ -23,14 +28,9 @@ jobs: - name: Install Conan id: conan uses: turtlebrowser/get-conan@main - with: - version: 1.59.0 - name: Create default profile - run: conan profile new default --detect - - - name: Update profile - run: conan profile update settings.compiler.libcxx=libstdc++11 default + run: conan profile detect - name: Create Build Environment # Some projects don't allow in-source building, so create a separate build directory @@ -44,7 +44,7 @@ jobs: - name: Configure CMake shell: bash working-directory: ${{github.workspace}}/build - run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake + run: cmake ${{github.workspace}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake - name: Build shell: bash @@ -52,8 +52,8 @@ jobs: run: cmake --build . --config ${{env.BUILD_TYPE}} - name: run test (Linux) - working-directory: ${{github.workspace}}/build - run: ./tests/behaviortree_cpp_test + working-directory: ${{github.workspace}}/build/tests + run: ctest - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 diff --git a/.github/workflows/cmake_windows.yml b/.github/workflows/cmake_windows.yml index 192fdcac6..34f4f97ce 100644 --- a/.github/workflows/cmake_windows.yml +++ b/.github/workflows/cmake_windows.yml @@ -1,6 +1,11 @@ name: cmake Windows -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) diff --git a/.github/workflows/doxygen-gh-pages.yml b/.github/workflows/doxygen-gh-pages.yml new file mode 100644 index 000000000..b8fb8f46b --- /dev/null +++ b/.github/workflows/doxygen-gh-pages.yml @@ -0,0 +1,18 @@ +name: Doxygen GitHub Pages Deploy Action + +on: + push: + branches: + - main + - master + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: DenverCoder1/doxygen-github-pages-action@v2.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + folder: doc/html diff --git a/.github/workflows/pixi.yaml b/.github/workflows/pixi.yaml index 81ee4b675..ddd1cbfb8 100644 --- a/.github/workflows/pixi.yaml +++ b/.github/workflows/pixi.yaml @@ -1,6 +1,11 @@ name: Pixi (conda) -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] jobs: pixi_conda_build: diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 6095e0861..ee7fa9229 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -1,9 +1,11 @@ name: pre-commit on: - pull_request: push: - branches: [master] + branches: + - master + pull_request: + types: [opened, synchronize, reopened] jobs: pre-commit: diff --git a/.github/workflows/ros1.yaml b/.github/workflows/ros1.yaml deleted file mode 100644 index 9d244f60f..000000000 --- a/.github/workflows/ros1.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: ros1 - -on: [push, pull_request] - -jobs: - industrial_ci: - strategy: - matrix: - env: - - {ROS_DISTRO: noetic, ROS_REPO: main} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: 'ros-industrial/industrial_ci@master' - env: ${{matrix.env}} - with: - package-name: behaviortree_cpp diff --git a/.github/workflows/ros2-rolling.yaml b/.github/workflows/ros2-rolling.yaml index cb5004506..446c49879 100644 --- a/.github/workflows/ros2-rolling.yaml +++ b/.github/workflows/ros2-rolling.yaml @@ -1,6 +1,11 @@ name: ros2-rolling -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] jobs: industrial_ci: diff --git a/.github/workflows/ros2.yaml b/.github/workflows/ros2.yaml index afdccc466..099cc04f2 100644 --- a/.github/workflows/ros2.yaml +++ b/.github/workflows/ros2.yaml @@ -1,6 +1,11 @@ name: ros2 -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] jobs: industrial_ci: @@ -8,7 +13,7 @@ jobs: matrix: env: - {ROS_DISTRO: humble, ROS_REPO: main} - - {ROS_DISTRO: iron, ROS_REPO: main} + - {ROS_DISTRO: jazzy, ROS_REPO: main} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/sonarcube.yml b/.github/workflows/sonarcube.yml.bkp similarity index 99% rename from .github/workflows/sonarcube.yml rename to .github/workflows/sonarcube.yml.bkp index 29d83fb4a..926306125 100644 --- a/.github/workflows/sonarcube.yml +++ b/.github/workflows/sonarcube.yml.bkp @@ -1,10 +1,12 @@ name: Sonarcube Scan + on: push: branches: - master pull_request: types: [opened, synchronize, reopened] + jobs: build: name: Build diff --git a/.gitignore b/.gitignore index cba22ead7..9d5bd4326 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ CMakeSettings.json .pixi CMakeUserPresets.json + +tags diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6ace8262..d491f36d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,3 +42,13 @@ repos: hooks: - id: clang-format args: ['-fallback-style=none', '-i'] + + # Spell check + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + additional_dependencies: + - tomli + args: + [--toml=./pyproject.toml] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 49aa120c2..600abbaa3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,83 @@ Changelog for package behaviortree_cpp ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4.7.2 (2025-05-29) +------------------ +* Fix issue `#978 `_ : skipped was not working properly +* Added codespell as a pre-commit hook. (`#977 `_) +* fix: Make impossible to accidentally copy JsonExporter singleton (`#975 `_) +* Contributors: Davide Faconti, Leander Stephen D'Souza, tony-p + +4.7.1 (2025-05-13) +------------------ +* fix ROS CI +* Add action to publish Doxygen documentation as GH Page (`#972 `_) +* Update Doxyfile +* Make BT::Any::copyInto const (`#970 `_) +* more changes related to TestNode +* Contributors: David Sobek, Davide Faconti, Marcus Ebner von Eschenbach + +4.7.0 (2025-04-24) +------------------ +* change TestNodeConfig preferred constructor +* Fix dangling‐capture in TestNodeConfig +* Fix Precondition to only check condition once (`#904 `_) +* fix issue 945 +* extend JSON conversion to include vectors (`#965 `_) +* Fix CI, add BUILD_TESTS and remove catkin support +* Fix testing CMake issue to resolve Rolling regression (`#961 `_) +* Bug fix/set blackboard (`#955 `_) +* feat: add fuzzing harnesses (`#925 `_) +* fix warnings +* Add const to applyVisitor (`#935 `_) +* try fix (`#941 `_) +* add workflow for sonarcube (`#936 `_) +* Fix issue `#909 `_: static queue in Loop +* apply changes suggested in `#893 `_ +* apply fix mentioned in `#916 `_ +* apply fixes suggested in `#919 `_ +* fix issue `#918 `_ (introduced in `#885 `_) +* add fix suggested in `#920 `_ +* add unit test related to `#931 `_ +* Fix compilation error when targeting C++23 (`#926 `_) ^~~~~~~~~~~~~ +* Fixes issue # `#929 `_ and `#921 `_ +* apply check suggested in `#924 `_ +* Fix ROS 2 build when ZeroMQ or SQlite3 include are not in the default include path (`#911 `_) + * Fix ROS 2 build when ZeroMQ or SQlite3 include are not in the default include path + * Update ament_build.cmake +* Fix/use correct compiler pixi/conda (`#914 `_) + * fix: Use the cxx-compiler package which will set the correct compiler for the platform, and setup the required environment for it to work as expected + * misc: update pixi versions in pipeline +* Add "other ports" to NodeConfig (`#910 `_) +* [retry_node] Refresh max_attempts\_ in case it changed (`#905 `_) + Co-authored-by: Guillaume Doisy +* use relative path in .Doxyfile (`#882 `_) +* Additional XML verification for ReactiveSequence nodes (`#885 `_) + Co-authored-by: AndyZe +* fix script parse error while 'A==-1' (`#896 `_) + Co-authored-by: wangzheng +* Expose return value of wait_for (`#887 `_) +* fix(examples): update t11_groot_howto log filename (`#886 `_) +* put minitrace in the build_interface link library (`#874 `_) + fixes the cmake export set when building behavior tree on standard cmake: CMake Error: install(EXPORT "behaviortree_cppTargets" ...) includes target "behaviortree_cpp" which requires target "minitrace" that is not in any export set. +* Improved XML parsing error message to say where in the XML the offending port is found. (`#876 `_) + Example output: + a port with name [ball_pose] is found in the XML (, line 7) but not in the providedPorts() of its registered node type. +* Refactored the TreeNode::executeTick() function to use a scoped timer for performance monitoring. (`#861 `_) (`#863 `_) + Update src/tree_node.cpp + Co-authored-by: wangzheng + Co-authored-by: Davide Faconti +* fix issue `#852 `_: thread safety in Loggers +* Lexy updated +* tinyXML updated to version 10.0 +* cppzmq updated to version 4.10 +* fix the "all_skipped" logic +* fixed: support utf-8 path xml-file (`#845 `_) + * fixed: 1. added compile version check to support Chinese path xml-file parsing 2. cmake add msvc /utf-8 options + * change cmake /utf-8 option add mode +* Export plugins to share directory & register CrossDoor plugin (`#804 `_) +* Contributors: Aglargil, AndyZe, Antoine Hoarau, David Sobek, Davide Faconti, Guillaume Doisy, Isar Meijer, Jake Keller, Marq Rasmussen, Michele Tartari, Silvio Traversaro, Tony Najjar, b-adkins, ckrah, devis12, kinly, tony-p, vincent-hui + 4.6.2 (2024-06-26) ------------------ * Initialize template variable `T out` (`#839 `_) @@ -46,7 +123,7 @@ Changelog for package behaviortree_cpp * warn about overwritten enums * fix ambiguous to_json * Extend unit test for blackboard backup to run the second tree (`#789 `_) -* json convertion changed and +* json conversion changed and * issue `#755 `_ : add backchaining test and change reactive nodes checks (`#770 `_) * Update switch_node.h * test moved and port remapping fixed @@ -323,7 +400,7 @@ Changelog for package behaviortree_cpp * better include paths * Control node and Decorators RUNNING before first child * blackboard: update getKeys and add mutex to scripting -* add [[nodiscard]] and some othe minor changes +* add [[nodiscard]] and some other minor changes * add screenshot * change the behavior of tickOnce to actually loop is wake up signal is… (`#522 `_) * change the behavior of tickOnce to actually loop is wake up signal is received @@ -422,7 +499,7 @@ Changelog for package behaviortree_cpp dependency explicitly. * Change order of lock to prevent deadlock. (`#368 `_) Resolves `#367 `_. -* Fix `#320 `_ : forbit refrences in Any +* Fix `#320 `_ : forbid references in Any * Update action_node.h * Contributors: Adam Sasine, Davide Faconti, Fabian Schurig, Griswald Brooks, Hyeongsik Min, Robodrome, imgbot[bot], panwauu @@ -769,9 +846,9 @@ Changelog for package behaviortree_cpp * Conan package distribution (#39) * Non-functional refactoring of xml_parsing to clean up the code * cosmetic changes in the code of BehaviorTreeFactory -* XML schema. Related to enchancement #40 +* XML schema. Related to enhancement #40 * call setRegistrationName() for built-in Nodes - The methos is called by BehaviorTreefactory, therefore it + The method is called by BehaviorTreefactory, therefore it registrationName is empty if trees are created programmatically. * Reset reference count when destroying logger (issue #38) * Contributors: Davide Facont, Davide Faconti, Uilian Ries @@ -787,7 +864,7 @@ Changelog for package behaviortree_cpp ------------------ * adding virtual TreeNode::onInit() [issue #33] * fix issue #34 : if you don't implement convertFromString, it will compile but it may throw -* Pretty demangled names and obsolate comments removed +* Pretty demangled names and obsolete comments removed * bug fixes * more comments * [enhancement #32]: add CoroActionNode and rename ActionNode as "AsynActionNode" @@ -854,7 +931,7 @@ Changelog for package behaviortree_cpp * Fix: registerBuilder did not register the manifest. It was "broken" as public API method * Use the Pimpl idiom to hide zmq from the header file * move header of minitrace in the cpp file -* Fixed a crash occuring when you didn't initialized a Tree object (#20) +* Fixed a crash occurring when you didn't initialized a Tree object (#20) * Fix issue #16 * add ParallelNode to pre-registered entries in factory (issue #13) * removed M_PI diff --git a/CMakeLists.txt b/CMakeLists.txt index 54451c6a9..e69c9e96c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,19 +1,54 @@ cmake_minimum_required(VERSION 3.16.3) # version on Ubuntu Focal -project(behaviortree_cpp VERSION 4.6.2 LANGUAGES C CXX) +project(behaviortree_cpp VERSION 4.7.2 LANGUAGES C CXX) -set(CMAKE_CONFIG_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake") +# create compile_commands.json +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +#---- project configuration ---- +option(BTCPP_SHARED_LIBS "Build shared libraries" ON) +option(BTCPP_BUILD_TOOLS "Build commandline tools" ON) +option(BTCPP_EXAMPLES "Build tutorials and examples" ON) +option(BUILD_TESTING "Build the unit tests" ON) +option(BTCPP_GROOT_INTERFACE "Add Groot2 connection. Requires ZeroMQ" ON) +option(BTCPP_SQLITE_LOGGING "Add SQLite logging." ON) + +option(USE_V3_COMPATIBLE_NAMES "Use some alias to compile more easily old 3.x code" OFF) +option(ENABLE_FUZZING "Enable fuzzing builds" OFF) +option(USE_AFLPLUSPLUS "Use AFL++ instead of libFuzzer" OFF) +option(ENABLE_DEBUG "Enable debug build with full symbols" OFF) +option(FORCE_STATIC_LINKING "Force static linking of all dependencies" OFF) + +set(BASE_FLAGS "") + +if(ENABLE_DEBUG) + list(APPEND BASE_FLAGS + -g3 + -ggdb3 + -O0 + -fno-omit-frame-pointer + ) +endif() + +# Include fuzzing configuration if enabled +if(ENABLE_FUZZING) + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/fuzzing_build.cmake) +else() + # Apply base flags for non-fuzzing builds + add_compile_options(${BASE_FLAGS}) + add_link_options(${BASE_FLAGS}) +endif() + +set(CMAKE_CONFIG_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CONFIG_PATH}") set(BTCPP_LIBRARY ${PROJECT_NAME}) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - message(STATUS "Setting build type to 'Release' as none was specified.") - set(CMAKE_BUILD_TYPE "Release" CACHE - STRING "Choose the type of build." FORCE) - # Set the possible values of build type for cmake-gui - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Release" "MinSizeRel" "RelWithDebInfo") + message(STATUS "Setting build type to 'Release' as none was specified.") + set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() if(MSVC) @@ -22,20 +57,6 @@ else() add_definitions(-Wpedantic -fno-omit-frame-pointer) endif() -# create compile_commands.json -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - - -#---- project configuration ---- -option(BTCPP_SHARED_LIBS "Build shared libraries" ON) -option(BTCPP_BUILD_TOOLS "Build commandline tools" ON) -option(BTCPP_EXAMPLES "Build tutorials and examples" ON) -option(BTCPP_UNIT_TESTS "Build the unit tests" ON) -option(BTCPP_GROOT_INTERFACE "Add Groot2 connection. Requires ZeroMQ" ON) -option(BTCPP_SQLITE_LOGGING "Add SQLite logging." ON) - -option(USE_V3_COMPATIBLE_NAMES "Use some alias to compile more easily old 3.x code" OFF) - if(USE_V3_COMPATIBLE_NAMES) add_definitions(-DUSE_BTCPP3_OLD_NAMES) endif() @@ -58,21 +79,12 @@ if ( ament_cmake_FOUND ) add_definitions( -DUSING_ROS2 ) message(STATUS "------------------------------------------") - message(STATUS "BehaviourTree is being built using AMENT.") + message(STATUS "BehaviorTree is being built using AMENT.") message(STATUS "------------------------------------------") include(cmake/ament_build.cmake) - -elseif( CATKIN_DEVEL_PREFIX OR CATKIN_BUILD_BINARY_PACKAGE) - - add_definitions( -DUSING_ROS ) - message(STATUS "------------------------------------------") - message(STATUS "BehaviourTree is being built using CATKIN.") - message(STATUS "------------------------------------------") - include(cmake/catkin_build.cmake) - set(catkin_FOUND TRUE) else() message(STATUS "------------------------------------------") - message(STATUS "BehaviourTree is being built with conan.") + message(STATUS "BehaviorTree is being built with conan.") message(STATUS "------------------------------------------") include(cmake/conan_build.cmake) endif() @@ -189,25 +201,35 @@ target_compile_definitions(${BTCPP_LIBRARY} PUBLIC BTCPP_LIBRARY_VERSION="${CMAK target_compile_features(${BTCPP_LIBRARY} PUBLIC cxx_std_17) if(MSVC) - target_compile_options(${BTCPP_LIBRARY} PRIVATE "/source-charset:utf-8") + target_compile_options(${BTCPP_LIBRARY} PRIVATE "/source-charset:utf-8") else() - target_compile_options(${BTCPP_LIBRARY} PRIVATE -Wall -Wextra) + if(ENABLE_DEBUG) + target_compile_options(${BTCPP_LIBRARY} PRIVATE -Wall -Wextra -g3 -ggdb3 -O0 -fno-omit-frame-pointer) + else() + target_compile_options(${BTCPP_LIBRARY} PRIVATE -Wall -Wextra) + endif() endif() add_library(BT::${BTCPP_LIBRARY} ALIAS ${BTCPP_LIBRARY}) +# Add fuzzing targets +if(ENABLE_FUZZING) + add_fuzzing_targets() +endif() + ############################################################# message( STATUS "BTCPP_LIB_DESTINATION: ${BTCPP_LIB_DESTINATION} " ) message( STATUS "BTCPP_INCLUDE_DESTINATION: ${BTCPP_INCLUDE_DESTINATION} " ) -message( STATUS "BTCPP_UNIT_TESTS: ${BTCPP_UNIT_TESTS} " ) -if (BTCPP_UNIT_TESTS OR BTCPP_EXAMPLES) -add_subdirectory(sample_nodes) +if (BUILD_TESTING OR BTCPP_EXAMPLES) + add_subdirectory(sample_nodes) endif() ###################################################### -if (BTCPP_UNIT_TESTS) +include(CTest) +message( STATUS "BUILD_TESTING: ${BUILD_TESTING} " ) +if (BUILD_TESTING) add_subdirectory(tests) endif() diff --git a/Doxyfile b/Doxyfile index 353afcde5..d06db140e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -864,7 +864,8 @@ RECURSIVE = YES # run. EXCLUDE = ./3rdparty \ - ./gtest + ./gtest \ + ./include/behaviortree_cpp/contrib # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded diff --git a/README.md b/README.md index cd8e3371a..6fda200c7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ ![License MIT](https://img.shields.io/github/license/BehaviorTree/BehaviorTree.CPP?color=blue) -![Version](https://img.shields.io/badge/version-4.6-blue.svg) [![conan Ubuntu](https://github.com/BehaviorTree/BehaviorTree.CPP/actions/workflows/cmake_ubuntu.yml/badge.svg)](https://github.com/BehaviorTree/BehaviorTree.CPP/actions/workflows/cmake_ubuntu.yml) [![conan Windows](https://github.com/BehaviorTree/BehaviorTree.CPP/actions/workflows/cmake_windows.yml/badge.svg)](https://github.com/BehaviorTree/BehaviorTree.CPP/actions/workflows/cmake_windows.yml) -[![ros1](https://github.com/BehaviorTree/BehaviorTree.CPP/workflows/ros1/badge.svg?branch=master)](https://github.com/BehaviorTree/BehaviorTree.CPP/actions?query=workflow%3Aros1) -[![ros2](https://github.com/BehaviorTree/BehaviorTree.CPP/workflows/ros2/badge.svg?branch=master)](https://github.com/BehaviorTree/BehaviorTree.CPP/actions?query=workflow%3Aros2) +[![ros2](https://github.com/BehaviorTree/BehaviorTree.CPP/actions/workflows/ros2.yaml/badge.svg)](https://github.com/BehaviorTree/BehaviorTree.CPP/actions/workflows/ros2.yaml) [![pixi (Conda)](https://github.com/BehaviorTree/BehaviorTree.CPP/actions/workflows/pixi.yaml/badge.svg)](https://github.com/BehaviorTree/BehaviorTree.CPP/actions/workflows/pixi.yaml) -# BehaviorTree.CPP 4.6 +# BehaviorTree.CPP 4.7

@@ -37,6 +35,8 @@ to visualize, record, replay and analyze state transitions. You can learn about the main concepts, the API and the tutorials here: https://www.behaviortree.dev/ +An automatically generated API documentation can be found here: https://BehaviorTree.github.io/BehaviorTree.CPP/ + If the documentation doesn't answer your questions and/or you want to connect with the other **BT.CPP** users, visit [our forum](https://github.com/BehaviorTree/BehaviorTree.CPP/discussions) @@ -55,7 +55,6 @@ If you are looking for a more fancy graphical user interface (and I know you do) Three build systems are supported: -- **catkin**, if you use ROS - **colcon (ament)**, if you use ROS2 - **conan** otherwise (Linux/Windows). - **straight cmake** if you want to be personally responsible for dependencies :) diff --git a/cmake/catkin_build.cmake b/cmake/catkin_build.cmake deleted file mode 100644 index 487d84773..000000000 --- a/cmake/catkin_build.cmake +++ /dev/null @@ -1,40 +0,0 @@ -#---- Add the subdirectory cmake ---- -set(CMAKE_CONFIG_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake") -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CONFIG_PATH}") - -if(BTCPP_GROOT_INTERFACE) - find_package(ZeroMQ REQUIRED) -endif() - -if(BTCPP_SQLITE_LOGGING) - find_package(SQLite3 REQUIRED) -endif() - -find_package(catkin REQUIRED COMPONENTS roslib) - -catkin_package( - INCLUDE_DIRS include - LIBRARIES ${BTCPP_LIBRARY} - CATKIN_DEPENDS roslib ) - -set(BTCPP_EXTRA_INCLUDE_DIRS ${catkin_INCLUDE_DIRS} ) - -set( BTCPP_EXTRA_LIBRARIES - ${catkin_LIBRARIES} - ${ZeroMQ_LIBRARIES} - ${SQLite3_LIBRARIES}) - -set( BTCPP_LIB_DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} ) -set( BTCPP_INCLUDE_DESTINATION ${CATKIN_GLOBAL_INCLUDE_DESTINATION} ) -set( BTCPP_BIN_DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION} ) - -mark_as_advanced( - BTCPP_EXTRA_LIBRARIES - BTCPP_EXTRA_INCLUDE_DIRS - BTCPP_LIB_DESTINATION - BTCPP_INCLUDE_DESTINATION - BTCPP_BIN_DESTINATION ) - -macro(export_btcpp_package) - # do nothing -endmacro() diff --git a/cmake/conan.cmake b/cmake/conan.cmake index 3fa9a26ba..d36c5ed44 100644 --- a/cmake/conan.cmake +++ b/cmake/conan.cmake @@ -116,7 +116,7 @@ macro(_conan_check_language) set(LANGUAGE C) set(USING_CXX 0) else () - message(FATAL_ERROR "Conan: Neither C or C++ was detected as a language for the project. Unabled to detect compiler version.") + message(FATAL_ERROR "Conan: Neither C or C++ was detected as a language for the project. Unable to detect compiler version.") endif() endmacro() @@ -1050,7 +1050,7 @@ macro(conan_config_install) endif() if(DEFINED CONAN_ARGS) - # Convert ; seperated multi arg list into space seperated string + # Convert ; separated multi arg list into space separated string string(REPLACE ";" " " l_CONAN_ARGS "${CONAN_ARGS}") set(CONAN_ARGS_ARGS "--args=${l_CONAN_ARGS}") endif() diff --git a/cmake/fuzzing_build.cmake b/cmake/fuzzing_build.cmake new file mode 100644 index 000000000..43f367001 --- /dev/null +++ b/cmake/fuzzing_build.cmake @@ -0,0 +1,153 @@ +# Fuzzing configuration +# Supports both local fuzzing and OSS-Fuzz integration + +# Detect if we're running in OSS-Fuzz environment +if(DEFINED ENV{LIB_FUZZING_ENGINE}) + set(OSS_FUZZ ON) + message(STATUS "OSS-Fuzz environment detected") +else() + set(OSS_FUZZ OFF) +endif() + +# Auto-detect AFL++ compiler if not in OSS-Fuzz mode +if(NOT OSS_FUZZ AND (CMAKE_C_COMPILER MATCHES ".*afl-.*" OR CMAKE_CXX_COMPILER MATCHES ".*afl-.*")) + set(USE_AFLPLUSPLUS ON CACHE BOOL "Use AFL++ instead of libFuzzer" FORCE) + message(STATUS "AFL++ compiler detected - automatically enabling AFL++ mode") +endif() + +# When building for fuzzing, we want static library by default +set(BTCPP_SHARED_LIBS OFF CACHE BOOL "Build static library for fuzzing" FORCE) + +# Only apply static linking settings if explicitly requested +if(FORCE_STATIC_LINKING) + set(CMAKE_FIND_LIBRARY_SUFFIXES .a ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(BUILD_SHARED_LIBS OFF) + + # Force static linking for dependencies + if(BTCPP_GROOT_INTERFACE) + set(ZeroMQ_USE_STATIC_LIBS ON) + set(ZEROMQ_STATIC_LIBRARY ON) + endif() + + if(BTCPP_SQLITE_LOGGING) + set(SQLite3_USE_STATIC_LIBS ON) + endif() +endif() + +# Set up flags for local fuzzing (not used for OSS-Fuzz) +if(NOT OSS_FUZZ) + list(APPEND BASE_FLAGS -O2) + + if(USE_AFLPLUSPLUS) + set(SANITIZER_FLAGS + -fsanitize=address,undefined + ) + else() + # For libFuzzer, use fuzzer-no-link for the library + set(SANITIZER_FLAGS + -fsanitize=address,undefined,fuzzer-no-link + ) + endif() + + # Apply sanitizer flags to the base library + list(APPEND BASE_FLAGS ${SANITIZER_FLAGS}) + + add_compile_options(${BASE_FLAGS}) + add_link_options(${BASE_FLAGS}) +endif() + +# Disable certain features during fuzzing +set(BTCPP_EXAMPLES OFF CACHE BOOL "Disable examples during fuzzing" FORCE) +set(BTCPP_BUILD_TOOLS OFF CACHE BOOL "Disable tools during fuzzing" FORCE) +set(BTCPP_UNIT_TESTS OFF CACHE BOOL "Disable tests during fuzzing" FORCE) +set(BTCPP_SHARED_LIBS OFF CACHE BOOL "Build static library for fuzzing" FORCE) + +# Function to apply fuzzing flags for local development builds +function(apply_local_fuzzing_flags target) + target_compile_options(${target} PRIVATE + ${BASE_FLAGS} + ${SANITIZER_FLAGS} + ) + + if(FORCE_STATIC_LINKING) + if(USE_AFLPLUSPLUS) + target_link_options(${target} PRIVATE + ${BASE_FLAGS} + ${SANITIZER_FLAGS} + -static-libstdc++ + -static-libgcc + -fsanitize=fuzzer + ) + else() + target_link_options(${target} PRIVATE + ${BASE_FLAGS} + -fsanitize=fuzzer + ${SANITIZER_FLAGS} + -static-libstdc++ + -static-libgcc + ) + endif() + else() + if(USE_AFLPLUSPLUS) + target_link_options(${target} PRIVATE + ${BASE_FLAGS} + ${SANITIZER_FLAGS} + -fsanitize=fuzzer + ) + else() + target_link_options(${target} PRIVATE + ${BASE_FLAGS} + -fsanitize=fuzzer + ${SANITIZER_FLAGS} + ) + endif() + endif() +endfunction() + +# Function to add fuzzing targets - compatible with both local and OSS-Fuzz builds +function(add_fuzzing_targets) + set(FUZZERS bt_fuzzer script_fuzzer bb_fuzzer) + + foreach(fuzzer ${FUZZERS}) + add_executable(${fuzzer} fuzzing/${fuzzer}.cpp) + + if(OSS_FUZZ) + # For OSS-Fuzz environment, we rely on environment variables + # like $CC, $CXX, $CFLAGS, $CXXFLAGS, and $LIB_FUZZING_ENGINE + target_link_libraries(${fuzzer} PRIVATE + ${BTCPP_LIBRARY} + ${BTCPP_EXTRA_LIBRARIES} + $ENV{LIB_FUZZING_ENGINE} + ) + else() + # For local development, use our own flags + apply_local_fuzzing_flags(${fuzzer}) + target_link_libraries(${fuzzer} PRIVATE + ${BTCPP_LIBRARY} + ${BTCPP_EXTRA_LIBRARIES} + ) + endif() + + # Setup corpus directories (useful for both environments) + set(CORPUS_DIR ${CMAKE_BINARY_DIR}/corpus/${fuzzer}) + file(MAKE_DIRECTORY ${CORPUS_DIR}) + endforeach() + + # Copy corpus files if they exist (useful for local testing) + # OSS-Fuzz provides its own corpus handling + if(NOT OSS_FUZZ) + file(GLOB BT_CORPUS_FILES "${CMAKE_SOURCE_DIR}/fuzzing/corpus/bt_corpus/*") + file(GLOB SCRIPT_CORPUS_FILES "${CMAKE_SOURCE_DIR}/fuzzing/corpus/script_corpus/*") + file(GLOB BB_CORPUS_FILES "${CMAKE_SOURCE_DIR}/fuzzing/corpus/bb_corpus/*") + + if(BT_CORPUS_FILES) + file(COPY ${BT_CORPUS_FILES} DESTINATION ${CMAKE_BINARY_DIR}/corpus/bt_fuzzer) + endif() + if(SCRIPT_CORPUS_FILES) + file(COPY ${SCRIPT_CORPUS_FILES} DESTINATION ${CMAKE_BINARY_DIR}/corpus/script_fuzzer) + endif() + if(BB_CORPUS_FILES) + file(COPY ${BB_CORPUS_FILES} DESTINATION ${CMAKE_BINARY_DIR}/corpus/bb_fuzzer) + endif() + endif() +endfunction() diff --git a/examples/t12_default_ports.cpp b/examples/t12_default_ports.cpp index 8fa866a93..91d2f72aa 100644 --- a/examples/t12_default_ports.cpp +++ b/examples/t12_default_ports.cpp @@ -22,7 +22,7 @@ struct Point2D } }; -// Allow bi-directional convertion to JSON +// Allow bi-directional conversion to JSON BT_JSON_CONVERTER(Point2D, point) { add_field("x", &point.x); diff --git a/examples/t15_nodes_mocking.cpp b/examples/t15_nodes_mocking.cpp index 28806da7e..caef9e224 100644 --- a/examples/t15_nodes_mocking.cpp +++ b/examples/t15_nodes_mocking.cpp @@ -97,7 +97,7 @@ int main(int argc, char** argv) // this will be synchronous (async_delay is 0) BT::TestNodeConfig counting_config; - test_config.return_status = BT::NodeStatus::SUCCESS; + counting_config.return_status = BT::NodeStatus::SUCCESS; //--------------------------------------------------------------- // Next, we want to substitute one or more of out Nodes with this mocks @@ -147,7 +147,7 @@ int main(int argc, char** argv) factory.loadSubstitutionRuleFromJSON(json_text); } //--------------------------------------------------------------- - // IMPORTANT: all substiutions must be done BEFORE creating the tree + // IMPORTANT: all substitutions must be done BEFORE creating the tree // During the construction phase of the tree, the substitution // rules will be used to instantiate the test nodes, instead of the // original ones. @@ -158,7 +158,7 @@ int main(int argc, char** argv) return 0; } -/* Expecte output: +/* Expected output: ----- Nodes fullPath() ------- Sequence::1 diff --git a/examples/t16_global_blackboard.cpp b/examples/t16_global_blackboard.cpp index 956bbe2c2..38bf35b14 100644 --- a/examples/t16_global_blackboard.cpp +++ b/examples/t16_global_blackboard.cpp @@ -97,7 +97,7 @@ int main() return 0; } -/* Expecte output: +/* Expected output: [main_print] val: 1 [sub_print] val: 1 diff --git a/fuzzing/README.md b/fuzzing/README.md new file mode 100644 index 000000000..e42a75bd0 --- /dev/null +++ b/fuzzing/README.md @@ -0,0 +1,21 @@ +# Fuzzing BehaviorTree.CPP + +You can build the existing harnesses either for libfuzzer or AFL++. +Building the fuzzers requires `clang` (libfuzzer) or an installed version +of [AFL++](https://github.com/AFLplusplus/AFLplusplus). + +## libfuzzer + +```bash +mkdir build_libfuzzer && cd build_libfuzzer +cmake -DENABLE_FUZZING=ON .. +``` + +## AFL++ + +```bash +export CC=afl-clang-fast +export CXX=afl-clang-fast++ +mkdir build_afl && cd build_afl +cmake -DENABLE_FUZZING=ON -DUSE_AFLPLUSPLUS=ON .. +``` diff --git a/fuzzing/bb_fuzzer.cpp b/fuzzing/bb_fuzzer.cpp new file mode 100644 index 000000000..5667d8ffb --- /dev/null +++ b/fuzzing/bb_fuzzer.cpp @@ -0,0 +1,270 @@ +#include "behaviortree_cpp/blackboard.h" +#include +#include +#include +#include +#include + +#include + +class ExceptionFilter +{ +public: + static bool isExpectedException(const std::exception& e) + { + const std::string what = e.what(); + const std::vector expected_patterns = { "Blackboard::set", + "once declared, the type of a " + "port shall not change", + "Missing key", + "hasn't been initialized", + "Missing parent blackboard", + "Floating point truncated", + "Value outside the max " + "numerical limit", + "Value outside the lovest " + "numerical limit", + "Value is negative and can't be " + "converted to unsigned", + "Implicit casting to bool is " + "not allowed" }; + + for(const auto& pattern : expected_patterns) + { + if(what.find(pattern) != std::string::npos) + { + return true; + } + } + return false; + } +}; + +class BlackboardFuzzer +{ +private: + std::vector blackboards_; + std::vector generated_keys_; + FuzzedDataProvider& fuzz_data_; + + std::string generateKey() + { + const std::string key_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01" + "23456789_@"; + size_t length = fuzz_data_.ConsumeIntegralInRange(1, 32); + std::string key; + for(size_t i = 0; i < length; ++i) + { + key += + key_chars[fuzz_data_.ConsumeIntegralInRange(0, key_chars.length() - 1)]; + } + generated_keys_.push_back(key); + return key; + } + + void fuzzSingleBB(BT::Blackboard::Ptr bb) + { + if(!bb) + return; + + try + { + // Create random entry + std::string key = generateKey(); + switch(fuzz_data_.ConsumeIntegralInRange(0, 6)) + { + case 0: + bb->set(key, fuzz_data_.ConsumeIntegral()); + break; + case 1: + bb->set(key, fuzz_data_.ConsumeFloatingPoint()); + break; + case 2: + bb->set(key, fuzz_data_.ConsumeRandomLengthString()); + break; + case 3: + bb->set(key, fuzz_data_.ConsumeBool()); + break; + case 4: + bb->set(key, fuzz_data_.ConsumeIntegral()); + break; + case 5: + bb->set(key, fuzz_data_.ConsumeFloatingPoint()); + break; + case 6: { + // Try to get non-existent key + bb->get(generateKey()); + break; + } + } + + // Random operations on existing keys + if(!generated_keys_.empty()) + { + const auto& existing_key = + generated_keys_[fuzz_data_.ConsumeIntegralInRange( + 0, generated_keys_.size() - 1)]; + + switch(fuzz_data_.ConsumeIntegralInRange(0, 4)) + { + case 0: + bb->unset(existing_key); + break; + case 1: + bb->getEntry(existing_key); + break; + case 2: + bb->get(existing_key); + break; + case 3: + bb->get(existing_key); + break; + case 4: + bb->get(existing_key); + break; + } + } + + // Random remapping operations + if(generated_keys_.size() >= 2) + { + size_t idx1 = + fuzz_data_.ConsumeIntegralInRange(0, generated_keys_.size() - 1); + size_t idx2 = + fuzz_data_.ConsumeIntegralInRange(0, generated_keys_.size() - 1); + bb->addSubtreeRemapping(generated_keys_[idx1], generated_keys_[idx2]); + } + } + catch(const std::exception& e) + { + if(!ExceptionFilter::isExpectedException(e)) + { + throw; + } + } + } + + void createBlackboardHierarchy() + { + if(blackboards_.empty()) + return; + + auto parent = blackboards_[fuzz_data_.ConsumeIntegralInRange( + 0, blackboards_.size() - 1)]; + + auto child = BT::Blackboard::create(parent); + if(fuzz_data_.ConsumeBool()) + { + child->enableAutoRemapping(true); + } + + blackboards_.push_back(child); + } + + void fuzzJsonOperations(BT::Blackboard::Ptr bb) + { + try + { + auto json = BT::ExportBlackboardToJSON(*bb); + if(fuzz_data_.ConsumeBool()) + { + std::string json_str = json.dump(); + size_t pos = fuzz_data_.ConsumeIntegralInRange(0, json_str.length()); + json_str.insert(pos, fuzz_data_.ConsumeRandomLengthString()); + json = nlohmann::json::parse(json_str); + } + BT::ImportBlackboardFromJSON(json, *bb); + } + catch(const std::exception& e) + { + if(!ExceptionFilter::isExpectedException(e)) + { + throw; + } + } + } + +public: + explicit BlackboardFuzzer(FuzzedDataProvider& provider) : fuzz_data_(provider) + { + blackboards_.push_back(BT::Blackboard::create()); + } + + void fuzz() + { + size_t num_operations = fuzz_data_.ConsumeIntegralInRange(50, 200); + + for(size_t i = 0; i < num_operations && !blackboards_.empty(); ++i) + { + try + { + // Randomly select a blackboard to operate on + size_t bb_idx = + fuzz_data_.ConsumeIntegralInRange(0, blackboards_.size() - 1); + auto bb = blackboards_[bb_idx]; + + switch(fuzz_data_.ConsumeIntegralInRange(0, 3)) + { + case 0: + // Fuzz single blackboard operations + fuzzSingleBB(bb); + break; + + case 1: + // Create new blackboards in hierarchy + if(fuzz_data_.ConsumeBool()) + { + createBlackboardHierarchy(); + } + break; + + case 2: + // JSON operations + fuzzJsonOperations(bb); + break; + + case 3: + // Cleanup operations + if(fuzz_data_.ConsumeBool() && blackboards_.size() > 1) + { + size_t remove_idx = + fuzz_data_.ConsumeIntegralInRange(0, blackboards_.size() - 1); + blackboards_.erase(blackboards_.begin() + remove_idx); + } + break; + } + } + catch(const std::exception& e) + { + if(!ExceptionFilter::isExpectedException(e)) + { + std::cerr << "Unexpected exception: " << e.what() << std::endl; + throw; + } + } + } + } +}; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if(size < 64) + return 0; + + try + { + FuzzedDataProvider fuzz_data(data, size); + BlackboardFuzzer fuzzer(fuzz_data); + fuzzer.fuzz(); + } + catch(const std::exception& e) + { + if(!ExceptionFilter::isExpectedException(e)) + { + std::cerr << "Unexpected top-level exception: " << e.what() << std::endl; + return 1; + } + } + + return 0; +} diff --git a/fuzzing/bt_fuzzer.cpp b/fuzzing/bt_fuzzer.cpp new file mode 100644 index 000000000..7411109cc --- /dev/null +++ b/fuzzing/bt_fuzzer.cpp @@ -0,0 +1,125 @@ +#include +#include "behaviortree_cpp/bt_factory.h" +#include "behaviortree_cpp/xml_parsing.h" +#include + +// List of valid node types we can use to construct valid-ish XML +constexpr const char* NODE_TYPES[] = { + "Sequence", "Fallback", "ParallelAll", + "ReactiveSequence", "ReactiveFallback", "IfThenElse", + "WhileDoElse", "Inverter", "RetryUntilSuccessful", + "Repeat", "Timeout", "Delay", + "ForceSuccess", "ForceFailure", "AlwaysSuccess", + "AlwaysFailure", "SetBlackboard", "SubTree" +}; + +// Attributes that can be added to nodes +constexpr const char* NODE_ATTRIBUTES[] = { "name", "ID", "port_1", + "port_2", "timeout_ms", "delay_ms", + "threshold", "max_repeats" }; + +std::string generateFuzzedNodeXML(FuzzedDataProvider& fdp, int depth = 0) +{ + // Prevent stack overflow with max depth + if(depth > 6) + { // Reasonable limit for XML tree depth + return ""; + } + + std::string xml; + const std::string node_type = fdp.PickValueInArray(NODE_TYPES); + + xml += "<" + node_type; + + size_t num_attributes = fdp.ConsumeIntegralInRange(0, 3); + for(size_t i = 0; i < num_attributes; i++) + { + const std::string attr = fdp.PickValueInArray(NODE_ATTRIBUTES); + std::string value = fdp.ConsumeRandomLengthString(10); + xml += " " + attr + "=\"" + value + "\""; + } + + if(depth > 3 || fdp.ConsumeBool()) + { + xml += "/>"; + } + else + { + xml += ">"; + // Add some child nodes recursively with depth limit + size_t num_children = fdp.ConsumeIntegralInRange(0, 2); + for(size_t i = 0; i < num_children; i++) + { + xml += generateFuzzedNodeXML(fdp, depth + 1); + } + xml += ""; + } + + return xml; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if(size < 4) + { + return 0; + } + + FuzzedDataProvider fdp(data, size); + BT::BehaviorTreeFactory factory; + + try + { + // Strategy 1: Test with completely random data + if(fdp.ConsumeBool()) + { + std::string random_xml = fdp.ConsumeRandomLengthString(size - 1); + try + { + factory.createTreeFromText(random_xml); + } + catch(const std::exception&) + {} + } + // Strategy 2: Generate semi-valid XML + else + { + std::string xml = R"( + + )"; + + size_t num_nodes = fdp.ConsumeIntegralInRange(1, 5); + for(size_t i = 0; i < num_nodes; i++) + { + xml += generateFuzzedNodeXML(fdp); + } + + xml += R"( + + )"; + + auto blackboard = BT::Blackboard::create(); + + switch(fdp.ConsumeIntegralInRange(0, 2)) + { + case 0: + factory.createTreeFromText(xml, blackboard); + break; + case 1: + BT::VerifyXML(xml, {}); + break; + case 2: + factory.registerBehaviorTreeFromText(xml); + if(!factory.registeredBehaviorTrees().empty()) + { + factory.createTree(factory.registeredBehaviorTrees().front(), blackboard); + } + break; + } + } + } + catch(const std::exception&) + {} + + return 0; +} diff --git a/fuzzing/corpus/bb_corpus/basic.json b/fuzzing/corpus/bb_corpus/basic.json new file mode 100644 index 000000000..9121b1350 --- /dev/null +++ b/fuzzing/corpus/bb_corpus/basic.json @@ -0,0 +1,6 @@ +{ + "int_key": 42, + "double_key": 3.14159, + "string_key": "test_string", + "bool_key": true +} diff --git a/fuzzing/corpus/bb_corpus/edge.json b/fuzzing/corpus/bb_corpus/edge.json new file mode 100644 index 000000000..8008ef811 --- /dev/null +++ b/fuzzing/corpus/bb_corpus/edge.json @@ -0,0 +1,7 @@ +{ + "max_int": 2147483647, + "min_int": -2147483648, + "zero": 0, + "empty_string": "", + "null_value": null +} diff --git a/fuzzing/corpus/bb_corpus/multiple.json b/fuzzing/corpus/bb_corpus/multiple.json new file mode 100644 index 000000000..cf5cfa76b --- /dev/null +++ b/fuzzing/corpus/bb_corpus/multiple.json @@ -0,0 +1,8 @@ +{ + "key1": 42, + "key2": "string", + "key3": 3.14, + "key4": true, + "key5": -123, + "key6": 99.99 +} diff --git a/fuzzing/corpus/bb_corpus/nested.json b/fuzzing/corpus/bb_corpus/nested.json new file mode 100644 index 000000000..39c4b000e --- /dev/null +++ b/fuzzing/corpus/bb_corpus/nested.json @@ -0,0 +1,11 @@ +{ + "outer_key": { + "inner_int": 100, + "inner_string": "nested" + }, + "array_key": [ + 1, + 2, + 3 + ] +} diff --git a/fuzzing/corpus/bb_corpus/special.json b/fuzzing/corpus/bb_corpus/special.json new file mode 100644 index 000000000..76a6a4b70 --- /dev/null +++ b/fuzzing/corpus/bb_corpus/special.json @@ -0,0 +1,6 @@ +{ + "special@key": 123, + "key_with_underscore": "value", + "KeyWithCaps": true, + "123numeric_start": 456 +} diff --git a/fuzzing/corpus/bt_corpus/corpus1.xml b/fuzzing/corpus/bt_corpus/corpus1.xml new file mode 100644 index 000000000..e625cb41b --- /dev/null +++ b/fuzzing/corpus/bt_corpus/corpus1.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/fuzzing/corpus/bt_corpus/corpus2.xml b/fuzzing/corpus/bt_corpus/corpus2.xml new file mode 100644 index 000000000..461e53c64 --- /dev/null +++ b/fuzzing/corpus/bt_corpus/corpus2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/fuzzing/corpus/bt_corpus/corpus3.xml b/fuzzing/corpus/bt_corpus/corpus3.xml new file mode 100644 index 000000000..e21ea8adf --- /dev/null +++ b/fuzzing/corpus/bt_corpus/corpus3.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/fuzzing/corpus/bt_corpus/corpus4.xml b/fuzzing/corpus/bt_corpus/corpus4.xml new file mode 100644 index 000000000..3111d8be4 --- /dev/null +++ b/fuzzing/corpus/bt_corpus/corpus4.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fuzzing/corpus/bt_corpus/sample1 b/fuzzing/corpus/bt_corpus/sample1 new file mode 100644 index 000000000..932d7e835 --- /dev/null +++ b/fuzzing/corpus/bt_corpus/sample1 @@ -0,0 +1 @@ + diff --git a/fuzzing/corpus/script_corpus/sample1 b/fuzzing/corpus/script_corpus/sample1 new file mode 100644 index 000000000..36c81d57f --- /dev/null +++ b/fuzzing/corpus/script_corpus/sample1 @@ -0,0 +1,3 @@ +a : = 1; +b : = 2; +a + b diff --git a/fuzzing/corpus/script_corpus/sample2 b/fuzzing/corpus/script_corpus/sample2 new file mode 100644 index 000000000..dc1d11e2c --- /dev/null +++ b/fuzzing/corpus/script_corpus/sample2 @@ -0,0 +1,3 @@ +x : = true; +y : = false; +x and y diff --git a/fuzzing/corpus/script_corpus/sample3 b/fuzzing/corpus/script_corpus/sample3 new file mode 100644 index 000000000..39ce18d0c --- /dev/null +++ b/fuzzing/corpus/script_corpus/sample3 @@ -0,0 +1,2 @@ +str : = 'hello'; +len(str) diff --git a/fuzzing/script_fuzzer.cpp b/fuzzing/script_fuzzer.cpp new file mode 100644 index 000000000..b507dcf17 --- /dev/null +++ b/fuzzing/script_fuzzer.cpp @@ -0,0 +1,70 @@ +#include +#include "behaviortree_cpp/scripting/script_parser.hpp" +#include "behaviortree_cpp/blackboard.h" +#include "behaviortree_cpp/basic_types.h" +#include +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if(size < 4) + { + return 0; + } + + FuzzedDataProvider fuzz_data(data, size); + + try + { + BT::Ast::Environment env; + env.vars = BT::Blackboard::create(); + env.enums = std::make_shared(); + + // Add some test variables to the blackboard + env.vars->set("test_int", 42); + env.vars->set("test_double", 3.14); + env.vars->set("test_bool", true); + env.vars->set("test_string", std::string("test")); + + // Add some test enums + (*env.enums)["RUNNING"] = 0; + (*env.enums)["SUCCESS"] = 1; + (*env.enums)["FAILURE"] = 2; + + std::string script = fuzz_data.ConsumeRandomLengthString(); + + auto validation_result = BT::ValidateScript(script); + + if(!validation_result) + { + auto parsed_script = BT::ParseScript(script); + if(parsed_script) + { + try + { + auto result = parsed_script.value()(env); + + if(result.isNumber()) + { + volatile auto num = result.cast(); + } + + env.vars->set("result", result); + + BT::Any read_back; + env.vars->get("result", read_back); + } + catch(const BT::RuntimeError&) + {} + } + } + + BT::ParseScriptAndExecute(env, script); + } + catch(const std::exception&) + {} + + return 0; +} diff --git a/include/behaviortree_cpp/action_node.h b/include/behaviortree_cpp/action_node.h index 3f9b64bf4..409e57987 100644 --- a/include/behaviortree_cpp/action_node.h +++ b/include/behaviortree_cpp/action_node.h @@ -99,10 +99,10 @@ class SimpleActionNode : public SyncActionNode * IMPORTANT: this action is quite hard to implement correctly. * Please make sure that you know what you are doing. * - * - In your overriden tick() method, you must check periodically + * - In your overridden tick() method, you must check periodically * the result of the method isHaltRequested() and stop your execution accordingly. * - * - in the overriden halt() method, you can do some cleanup, but do not forget to + * - in the overridden halt() method, you can do some cleanup, but do not forget to * invoke the base class method ThreadedAction::halt(); * * - remember, with few exceptions, a halted ThreadedAction must return NodeStatus::IDLE. diff --git a/include/behaviortree_cpp/actions/pop_from_queue.hpp b/include/behaviortree_cpp/actions/pop_from_queue.hpp index a2ebb1fe1..34b905fde 100644 --- a/include/behaviortree_cpp/actions/pop_from_queue.hpp +++ b/include/behaviortree_cpp/actions/pop_from_queue.hpp @@ -20,7 +20,7 @@ /** * Template Action used in ex04_waypoints.cpp example. * - * Its purpose is to do make it easy to create while loops wich consume the elements of a queue. + * Its purpose is to do make it easy to create while loops which consume the elements of a queue. * * Note that modifying the queue is not thread safe, therefore the action that creates the queue * or push elements into it, must be Synchronous. @@ -47,7 +47,7 @@ struct ProtectedQueue * * We avoid this using reference semantic (wrapping the object in a shared_ptr). * Unfortunately, remember that this makes our access to the list not thread-safe! - * This is the reason why we add a mutex to be used when modyfying the ProtectedQueue::items + * This is the reason why we add a mutex to be used when modifying the ProtectedQueue::items * * */ @@ -95,7 +95,7 @@ class PopFromQueue : public SyncActionNode }; /** - * Get the size of a queue. Usefull is you want to write something like: + * Get the size of a queue. Useful when you want to write something like: * * * diff --git a/include/behaviortree_cpp/actions/set_blackboard_node.h b/include/behaviortree_cpp/actions/set_blackboard_node.h index 9882cea6f..05282c0c0 100644 --- a/include/behaviortree_cpp/actions/set_blackboard_node.h +++ b/include/behaviortree_cpp/actions/set_blackboard_node.h @@ -44,7 +44,7 @@ class SetBlackboardNode : public SyncActionNode static PortsList providedPorts() { - return { InputPort("value", "Value to be written int othe output_key"), + return { InputPort("value", "Value to be written into the output_key"), BidirectionalPort("output_key", "Name of the blackboard entry where the " "value should be written") }; } @@ -61,13 +61,16 @@ class SetBlackboardNode : public SyncActionNode const std::string value_str = config().input_ports.at("value"); StringView stripped_key; + BT::Any out_value; + + std::shared_ptr dst_entry = + config().blackboard->getEntry(output_key); + if(isBlackboardPointer(value_str, &stripped_key)) { const auto input_key = std::string(stripped_key); std::shared_ptr src_entry = config().blackboard->getEntry(input_key); - std::shared_ptr dst_entry = - config().blackboard->getEntry(output_key); if(!src_entry) { @@ -78,13 +81,35 @@ class SetBlackboardNode : public SyncActionNode config().blackboard->createEntry(output_key, src_entry->info); dst_entry = config().blackboard->getEntry(output_key); } - dst_entry->value = src_entry->value; + + out_value = src_entry->value; } else { - config().blackboard->set(output_key, value_str); + out_value = BT::Any(value_str); + } + + if(out_value.empty()) + return NodeStatus::FAILURE; + + // avoid type issues when port is remapped: current implementation of the set might be a little bit problematic for initialized on the fly values + // this still does not attack math issues + if(dst_entry && dst_entry->info.type() != typeid(std::string) && out_value.isString()) + { + try + { + out_value = dst_entry->info.parseString(out_value.cast()); + } + catch(const std::exception& e) + { + throw LogicError("Can't convert string [", out_value.cast(), + "] to type [", BT::demangle(dst_entry->info.type()), + "]: ", e.what()); + } } + config().blackboard->set(output_key, out_value); + return NodeStatus::SUCCESS; } }; diff --git a/include/behaviortree_cpp/actions/test_node.h b/include/behaviortree_cpp/actions/test_node.h index c144fd43a..9aaabb829 100644 --- a/include/behaviortree_cpp/actions/test_node.h +++ b/include/behaviortree_cpp/actions/test_node.h @@ -37,9 +37,9 @@ struct TestNodeConfig /// if async_delay > 0, this action become asynchronous and wait this amount of time std::chrono::milliseconds async_delay = std::chrono::milliseconds(0); - /// Function invoked when the action is completed. By default just return [return_status] - /// Override it to intorduce more comple cases - std::function complete_func = [this]() { return return_status; }; + /// Function invoked when the action is completed. + /// If not specified, the node will return [return_status] + std::function complete_func; }; /** @@ -50,21 +50,28 @@ struct TestNodeConfig * 3. Either complete immediately (synchronous action), or after a * given period of time (asynchronous action) * - * This behavior is changed by the parameters pased with TestNodeConfig. + * This behavior is changed by the parameters passed with TestNodeConfig. * * This particular node is created by the factory when TestNodeConfig is * added as a substitution rule: * - * TestNodeConfig test_config; + * auto test_config = std::make_shared(); * // change fields of test_config * factory.addSubstitutionRule(pattern, test_config); * - * See tutorial 11 for more details. + * See tutorial 15 for more details. */ class TestNode : public BT::StatefulActionNode { public: - TestNode(const std::string& name, const NodeConfig& config, TestNodeConfig test_config); + // This constructor is deprecated, because it may cause problems if TestNodeConfig::complete_func is capturing + // a reference to the TestNode, i.e. [this]. Use the constructor with std::shared_ptr instead. + // For more details, see https://github.com/BehaviorTree/BehaviorTree.CPP/pull/967 + [[deprecated("prefer the constructor with std::shared_ptr")]] TestNode( + const std::string& name, const NodeConfig& config, TestNodeConfig test_config); + + TestNode(const std::string& name, const NodeConfig& config, + std::shared_ptr test_config); static PortsList providedPorts() { @@ -80,7 +87,7 @@ class TestNode : public BT::StatefulActionNode NodeStatus onCompleted(); - TestNodeConfig _test_config; + std::shared_ptr _config; ScriptFunction _success_executor; ScriptFunction _failure_executor; ScriptFunction _post_executor; diff --git a/include/behaviortree_cpp/bt_factory.h b/include/behaviortree_cpp/bt_factory.h index c20385b3b..020c9ea1d 100644 --- a/include/behaviortree_cpp/bt_factory.h +++ b/include/behaviortree_cpp/bt_factory.h @@ -279,9 +279,8 @@ class BehaviorTreeFactory /** * @brief registerFromROSPlugins finds all shared libraries that export ROS plugins for behaviortree_cpp, and calls registerFromPlugin for each library. * @throws If not compiled with ROS support or if the library cannot load for any reason - * */ - void registerFromROSPlugins(); + [[deprecated("Removed support for ROS1")]] void registerFromROSPlugins(); /** * @brief registerBehaviorTreeFromFile. @@ -470,12 +469,13 @@ class BehaviorTreeFactory void clearSubstitutionRules(); - using SubstitutionRule = std::variant; + using SubstitutionRule = + std::variant>; /** * @brief addSubstitutionRule replace a node with another one when the tree is * created. - * If the rule ia a string, we will use a diferent node type (already registered) + * If the rule ia a string, we will use a different node type (already registered) * instead. * If the rule is a TestNodeConfig, a test node with that configuration will be created instead. * @@ -526,7 +526,7 @@ std::vector BlackboardBackup(const BT::Tree& tree); * @brief BlackboardRestore uses Blackboard::cloneInto to restore * all the blackboards of the tree * - * @param backup a vectror of blackboards + * @param backup a vector of blackboards * @param tree the destination */ void BlackboardRestore(const std::vector& backup, BT::Tree& tree); diff --git a/include/behaviortree_cpp/decorators/run_once_node.h b/include/behaviortree_cpp/decorators/run_once_node.h index a40d86cdb..a3083d97e 100644 --- a/include/behaviortree_cpp/decorators/run_once_node.h +++ b/include/behaviortree_cpp/decorators/run_once_node.h @@ -42,7 +42,7 @@ class RunOnceNode : public DecoratorNode { return { InputPort("then_skip", true, "If true, skip after the first execution, " - "otherwise return the same NodeStatus returned once bu the " + "otherwise return the same NodeStatus returned once by the " "child.") }; } diff --git a/include/behaviortree_cpp/flatbuffers/bt_flatbuffer_helper.h b/include/behaviortree_cpp/flatbuffers/bt_flatbuffer_helper.h index 426ccbe2d..6629cdf54 100644 --- a/include/behaviortree_cpp/flatbuffers/bt_flatbuffer_helper.h +++ b/include/behaviortree_cpp/flatbuffers/bt_flatbuffer_helper.h @@ -136,7 +136,7 @@ inline void CreateFlatbuffersBehaviorTree(flatbuffers::FlatBufferBuilder& builde builder.Finish(behavior_tree); } -/** Serialize manually the informations about state transition +/** Serialize manually the information about state transition * No flatbuffer serialization here */ inline SerializedTransition SerializeTransition(uint16_t UID, Duration timestamp, diff --git a/include/behaviortree_cpp/json_export.h b/include/behaviortree_cpp/json_export.h index 1d47b0877..dfe9b0fee 100644 --- a/include/behaviortree_cpp/json_export.h +++ b/include/behaviortree_cpp/json_export.h @@ -11,7 +11,7 @@ namespace BT { /** -* To add new type to the JSON library, you should follow these isntructions: +* To add new type to the JSON library, you should follow these instructions: * https://json.nlohmann.me/features/arbitrary_types/ * * Considering for instance the type: @@ -51,6 +51,10 @@ class JsonExporter public: static JsonExporter& get(); + // Delete copy constructors as can only be this one global instance. + JsonExporter& operator=(JsonExporter&&) = delete; + JsonExporter& operator=(JsonExporter&) = delete; + /** * @brief toJson adds the content of "any" to the JSON "destination". * @@ -80,22 +84,31 @@ class JsonExporter template Expected fromJson(const nlohmann::json& source) const; - /// Register new JSON converters with addConverter(). - /// You should have used first the macro BT_JSON_CONVERTER + /** + * @brief Register new JSON converters with addConverter(). + * You should used first the macro BT_JSON_CONVERTER. + * The conversions from/to vector are automatically registered. + */ template void addConverter(); /** * @brief addConverter register a to_json function that converts a json to a type T. + * The conversion to std:vector is automatically registered. * * @param to_json the function with signature void(const T&, nlohmann::json&) - * @param add_type if true, add a field called [__type] with the name ofthe type. - * */ + * @param add_type if true, add a field called [__type] with the name of the type. + */ template void addConverter(std::function to_json, bool add_type = true); - /// Register custom from_json converter directly. + /** + * @brief addConverter register a from_json function that converts a json to a type T. + * The conversions from std::vector is automatically registered. + * + * @param from_json the function with signature void(const nlohmann::json&, T&) + */ template void addConverter(std::function from_json); @@ -105,6 +118,7 @@ class JsonExporter std::unordered_map to_json_converters_; std::unordered_map from_json_converters_; + std::unordered_map from_json_array_converters_; std::unordered_map type_names_; }; @@ -129,6 +143,15 @@ inline Expected JsonExporter::fromJson(const nlohmann::json& source) const template inline void JsonExporter::addConverter() { + // we need to get the name of the type + nlohmann::json const js = T{}; + // we insert both the name obtained from JSON and demangle + if(js.contains("__type")) + { + type_names_.insert({ std::string(js["__type"]), BT::TypeInfo::Create() }); + } + type_names_.insert({ BT::demangle(typeid(T)), BT::TypeInfo::Create() }); + ToJonConverter to_converter = [](const BT::Any& entry, nlohmann::json& dst) { dst = *const_cast(entry).castPtr(); }; @@ -139,16 +162,23 @@ inline void JsonExporter::addConverter() return { BT::Any(value), BT::TypeInfo::Create() }; }; - // we need to get the name of the type - nlohmann::json const js = T{}; - // we insert both the name obtained from JSON and demangle - if(js.contains("__type")) - { - type_names_.insert({ std::string(js["__type"]), BT::TypeInfo::Create() }); - } - type_names_.insert({ BT::demangle(typeid(T)), BT::TypeInfo::Create() }); - from_json_converters_.insert({ typeid(T), from_converter }); + + //---- include vectors of T + ToJonConverter to_array_converter = [](const BT::Any& entry, nlohmann::json& dst) { + dst = *const_cast(entry).castPtr>(); + }; + to_json_converters_.insert({ typeid(std::vector), to_array_converter }); + + FromJonConverter from_array_converter = [](const nlohmann::json& src) -> Entry { + std::vector value; + for(const auto& item : src) + { + value.push_back(item.get()); + } + return { BT::Any(value), BT::TypeInfo::Create>() }; + }; + from_json_array_converters_.insert({ typeid(T), from_array_converter }); } template @@ -163,6 +193,18 @@ inline void JsonExporter::addConverter( } }; to_json_converters_.insert({ typeid(T), std::move(converter) }); + //--------------------------------------------- + // add the vector converter + auto vector_converter = [converter](const BT::Any& entry, nlohmann::json& json) { + auto& vec = *const_cast(entry).castPtr>(); + for(const auto& item : vec) + { + nlohmann::json item_json; + converter(BT::Any(item), item_json); + json.push_back(item_json); + } + }; + to_json_converters_.insert({ typeid(std::vector), std::move(vector_converter) }); } template @@ -176,6 +218,19 @@ JsonExporter::addConverter(std::function func) }; type_names_.insert({ BT::demangle(typeid(T)), BT::TypeInfo::Create() }); from_json_converters_.insert({ typeid(T), std::move(converter) }); + //--------------------------------------------- + // add the vector converter + auto vector_converter = [func](const nlohmann::json& json) -> Entry { + std::vector tmp; + for(const auto& item : json) + { + T item_tmp; + func(item, item_tmp); + tmp.push_back(item_tmp); + } + return { BT::Any(tmp), BT::TypeInfo::Create>() }; + }; + from_json_array_converters_.insert({ typeid(T), std::move(vector_converter) }); } template diff --git a/include/behaviortree_cpp/loggers/groot2_protocol.h b/include/behaviortree_cpp/loggers/groot2_protocol.h index 507f2a00a..5c27b5cb5 100644 --- a/include/behaviortree_cpp/loggers/groot2_protocol.h +++ b/include/behaviortree_cpp/loggers/groot2_protocol.h @@ -27,11 +27,11 @@ namespace BT::Monitor enum RequestType : uint8_t { - // Request the entire tree defintion as XML + // Request the entire tree definition as XML FULLTREE = 'T', - // Request the staus of all the nodes + // Request the status of all the nodes STATUS = 'S', - // retrieve the valus in a set of blackboards + // retrieve the values in a set of blackboards BLACKBOARD = 'B', // Groot requests the insertion of a hook diff --git a/include/behaviortree_cpp/scripting/operators.hpp b/include/behaviortree_cpp/scripting/operators.hpp index 3033767de..4d41b6a88 100644 --- a/include/behaviortree_cpp/scripting/operators.hpp +++ b/include/behaviortree_cpp/scripting/operators.hpp @@ -27,7 +27,7 @@ using SimpleString = SafeAny::SimpleString; using expr_ptr = std::shared_ptr; -// extended strin to number that consider enums and booleans +// extended string to number that consider enums and booleans inline double StringToDouble(const Any& value, const Environment& env) { const auto str = value.cast(); diff --git a/include/behaviortree_cpp/tree_node.h b/include/behaviortree_cpp/tree_node.h index 75ed0d063..1dc73986d 100644 --- a/include/behaviortree_cpp/tree_node.h +++ b/include/behaviortree_cpp/tree_node.h @@ -100,7 +100,7 @@ struct NodeConfig const TreeNodeManifest* manifest = nullptr; - // Numberic unique identifier + // Numeric unique identifier uint16_t uid = 0; // Unique human-readable name, that encapsulate the subtree // hierarchy, for instance, given 2 nested trees, it should be: @@ -211,7 +211,7 @@ class TreeNode * NodeStatus myCallback(TreeNode& node, NodeStatus status) * * This callback is executed AFTER the tick() and, if it returns SUCCESS or FAILURE, - * the value returned by the actual tick() is overriden with this one. + * the value returned by the actual tick() is overridden with this one. */ void setPostTickFunction(PostTickCallback callback); @@ -253,7 +253,7 @@ class TreeNode /** * @brief getInputStamped is similar to getInput(dey, destination), - * but it returne also the Timestamp object, that can be used to check if + * but it returns also the Timestamp object, that can be used to check if * a value was updated and when. * * @param key the name of the port. @@ -300,7 +300,7 @@ class TreeNode * @brief setOutput modifies the content of an Output port * @param key the name of the port. * @param value new value - * @return valid Result, if succesful. + * @return valid Result, if successful. */ template Result setOutput(const std::string& key, const T& value); diff --git a/include/behaviortree_cpp/utils/safe_any.hpp b/include/behaviortree_cpp/utils/safe_any.hpp index 21c33e1ff..ce94b2eb7 100644 --- a/include/behaviortree_cpp/utils/safe_any.hpp +++ b/include/behaviortree_cpp/utils/safe_any.hpp @@ -162,32 +162,7 @@ class Any [[nodiscard]] T* castPtr() { static_assert(!std::is_same_v, "The value has been casted internally to " - "[double]. " - "Use that instead"); - static_assert(!SafeAny::details::is_integer() || std::is_same_v, "The" - " va" - "lue" - " ha" - "s " - "bee" - "n " - "cas" - "ted" - " in" - "ter" - "nal" - "ly " - "to " - "[in" - "t64" - "_t]" - ". " - "Use" - " th" - "at " - "ins" - "tea" - "d"); + "[double]. Use that instead"); return _any.empty() ? nullptr : linb::any_cast(&_any); } @@ -519,7 +494,7 @@ inline nonstd::expected Any::tryCast() const } // special case when the output is an enum. - // We will try first a int convertion + // We will try first a int conversion if constexpr(std::is_enum_v) { if(isNumber()) diff --git a/package.xml b/package.xml index c4cb75729..1d245b625 100644 --- a/package.xml +++ b/package.xml @@ -1,7 +1,7 @@ behaviortree_cpp - 4.6.2 + 4.7.2 This package provides the Behavior Trees core library. @@ -14,21 +14,19 @@ ros_environment - catkin - roslib + git + ament_cmake - ament_cmake - rclcpp - ament_index_cpp + rclcpp + ament_index_cpp libsqlite3-dev libzmq3-dev - ament_cmake_gtest + ament_cmake_gtest - catkin - ament_cmake + ament_cmake diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..3fc95ccc4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.codespell] +ignore-words = ".codespell_ignore_words" diff --git a/src/actions/test_node.cpp b/src/actions/test_node.cpp index 19b70b1aa..f2471c085 100644 --- a/src/actions/test_node.cpp +++ b/src/actions/test_node.cpp @@ -1,12 +1,20 @@ #include "behaviortree_cpp/actions/test_node.h" -BT::TestNode::TestNode(const std::string& name, const NodeConfig& config, - TestNodeConfig test_config) - : StatefulActionNode(name, config), _test_config(std::move(test_config)) +namespace BT +{ + +TestNode::TestNode(const std::string& name, const NodeConfig& config, + TestNodeConfig test_config) + : TestNode(name, config, std::make_shared(std::move(test_config))) +{} + +TestNode::TestNode(const std::string& name, const NodeConfig& config, + std::shared_ptr test_config) + : StatefulActionNode(name, config), _config(std::move(test_config)) { setRegistrationID("TestNode"); - if(_test_config.return_status == NodeStatus::IDLE) + if(_config->return_status == NodeStatus::IDLE) { throw RuntimeError("TestNode can not return IDLE"); } @@ -22,21 +30,21 @@ BT::TestNode::TestNode(const std::string& name, const NodeConfig& config, executor = result.value(); } }; - prepareScript(_test_config.success_script, _success_executor); - prepareScript(_test_config.failure_script, _failure_executor); - prepareScript(_test_config.post_script, _post_executor); + prepareScript(_config->success_script, _success_executor); + prepareScript(_config->failure_script, _failure_executor); + prepareScript(_config->post_script, _post_executor); } -BT::NodeStatus BT::TestNode::onStart() +NodeStatus TestNode::onStart() { - if(_test_config.async_delay <= std::chrono::milliseconds(0)) + if(_config->async_delay <= std::chrono::milliseconds(0)) { return onCompleted(); } // convert this in an asynchronous operation. Use another thread to count // a certain amount of time. _completed = false; - _timer.add(std::chrono::milliseconds(_test_config.async_delay), [this](bool aborted) { + _timer.add(std::chrono::milliseconds(_config->async_delay), [this](bool aborted) { if(!aborted) { _completed.store(true); @@ -50,7 +58,7 @@ BT::NodeStatus BT::TestNode::onStart() return NodeStatus::RUNNING; } -BT::NodeStatus BT::TestNode::onRunning() +NodeStatus TestNode::onRunning() { if(_completed) { @@ -59,16 +67,18 @@ BT::NodeStatus BT::TestNode::onRunning() return NodeStatus::RUNNING; } -void BT::TestNode::onHalted() +void TestNode::onHalted() { _timer.cancelAll(); } -BT::NodeStatus BT::TestNode::onCompleted() +NodeStatus TestNode::onCompleted() { Ast::Environment env = { config().blackboard, config().enums }; - auto status = _test_config.complete_func(); + auto status = + (_config->complete_func) ? _config->complete_func() : _config->return_status; + if(status == NodeStatus::SUCCESS && _success_executor) { _success_executor(env); @@ -83,3 +93,5 @@ BT::NodeStatus BT::TestNode::onCompleted() } return status; } + +} // namespace BT diff --git a/src/blackboard.cpp b/src/blackboard.cpp index bced7b5e2..305fdde6d 100644 --- a/src/blackboard.cpp +++ b/src/blackboard.cpp @@ -176,7 +176,7 @@ void Blackboard::cloneInto(Blackboard& dst) const auto it = dst_storage.find(src_key); if(it != dst_storage.end()) { - // overwite + // overwrite auto& dst_entry = it->second; dst_entry->string_converter = src_entry->string_converter; dst_entry->value = src_entry->value; diff --git a/src/bt_factory.cpp b/src/bt_factory.cpp index f66c3661f..63f6652ef 100644 --- a/src/bt_factory.cpp +++ b/src/bt_factory.cpp @@ -13,14 +13,9 @@ #include #include "behaviortree_cpp/bt_factory.h" #include "behaviortree_cpp/utils/shared_library.h" -#include "behaviortree_cpp/contrib/json.hpp" #include "behaviortree_cpp/xml_parsing.h" #include "wildcards/wildcards.hpp" -#ifdef USING_ROS -#include -#endif - namespace BT { @@ -196,66 +191,12 @@ void BehaviorTreeFactory::registerFromPlugin(const std::string& file_path) } } -#ifdef USING_ROS - -#ifdef _WIN32 -const char os_pathsep(';'); // NOLINT -#else -const char os_pathsep(':'); // NOLINT -#endif - -// This function is a copy from the one in class_loader_imp.hpp in ROS pluginlib -// package, licensed under BSD. -// https://github.com/ros/pluginlib -std::vector getCatkinLibraryPaths() -{ - std::vector lib_paths; - const char* env = std::getenv("CMAKE_PREFIX_PATH"); - if(env) - { - const std::string env_catkin_prefix_paths(env); - std::vector catkin_prefix_paths = - splitString(env_catkin_prefix_paths, os_pathsep); - for(BT::StringView catkin_prefix_path : catkin_prefix_paths) - { - std::filesystem::path path(static_cast(catkin_prefix_path)); - std::filesystem::path lib("lib"); - lib_paths.push_back((path / lib).string()); - } - } - return lib_paths; -} - -void BehaviorTreeFactory::registerFromROSPlugins() -{ - std::vector plugins; - ros::package::getPlugins("behaviortree_cpp", "bt_lib_plugin", plugins, true); - std::vector catkin_lib_paths = getCatkinLibraryPaths(); - - for(const auto& plugin : plugins) - { - auto filename = std::filesystem::path(plugin + BT::SharedLibrary::suffix()); - for(const auto& lib_path : catkin_lib_paths) - { - const auto full_path = std::filesystem::path(lib_path) / filename; - if(std::filesystem::exists(full_path)) - { - std::cout << "Registering ROS plugins from " << full_path.string() << std::endl; - registerFromPlugin(full_path.string()); - break; - } - } - } -} -#else - void BehaviorTreeFactory::registerFromROSPlugins() { throw RuntimeError("Using attribute [ros_pkg] in , but this library was " "compiled without ROS support. Recompile the BehaviorTree.CPP " "using catkin"); } -#endif void BehaviorTreeFactory::registerBehaviorTreeFromFile( const std::filesystem::path& filename) @@ -322,12 +263,22 @@ std::unique_ptr BehaviorTreeFactory::instantiateTreeNode( } else if(const auto test_config = std::get_if(&rule)) { - // second case, the varian is a TestNodeConfig - auto test_node = new TestNode(name, config, *test_config); - node.reset(test_node); + node = std::make_unique(name, config, + std::make_shared(*test_config)); + substituted = true; + break; + } + else if(const auto test_config = + std::get_if>(&rule)) + { + node = std::make_unique(name, config, *test_config); substituted = true; break; } + else + { + throw LogicError("Substitution rule is not a string or a TestNodeConfig"); + } } } diff --git a/src/controls/fallback_node.cpp b/src/controls/fallback_node.cpp index 1654e3f2a..4b8fb3afb 100644 --- a/src/controls/fallback_node.cpp +++ b/src/controls/fallback_node.cpp @@ -28,7 +28,7 @@ NodeStatus FallbackNode::tick() { const size_t children_count = children_nodes_.size(); - if(status() == NodeStatus::IDLE) + if(!isStatusActive(status())) { skipped_count_ = 0; } @@ -55,7 +55,7 @@ NodeStatus FallbackNode::tick() case NodeStatus::FAILURE: { current_child_idx_++; // Return the execution flow if the child is async, - // to make this interruptable. + // to make this interruptible. if(asynch_ && requiresWakeUp() && prev_status == NodeStatus::IDLE && current_child_idx_ < children_count) { @@ -77,19 +77,22 @@ NodeStatus FallbackNode::tick() } // end while loop // The entire while loop completed. This means that all the children returned FAILURE. + const bool all_children_skipped = (skipped_count_ == children_count); if(current_child_idx_ == children_count) { resetChildren(); current_child_idx_ = 0; + skipped_count_ = 0; } // Skip if ALL the nodes have been skipped - return (skipped_count_ == children_count) ? NodeStatus::SKIPPED : NodeStatus::FAILURE; + return (all_children_skipped) ? NodeStatus::SKIPPED : NodeStatus::FAILURE; } void FallbackNode::halt() { current_child_idx_ = 0; + skipped_count_ = 0; ControlNode::halt(); } diff --git a/src/controls/sequence_node.cpp b/src/controls/sequence_node.cpp index 708c18d18..483e66c5b 100644 --- a/src/controls/sequence_node.cpp +++ b/src/controls/sequence_node.cpp @@ -27,6 +27,7 @@ SequenceNode::SequenceNode(const std::string& name, bool make_async) void SequenceNode::halt() { current_child_idx_ = 0; + skipped_count_ = 0; ControlNode::halt(); } @@ -34,7 +35,7 @@ NodeStatus SequenceNode::tick() { const size_t children_count = children_nodes_.size(); - if(status() == NodeStatus::IDLE) + if(!isStatusActive(status())) { skipped_count_ = 0; } @@ -62,7 +63,7 @@ NodeStatus SequenceNode::tick() case NodeStatus::SUCCESS: { current_child_idx_++; // Return the execution flow if the child is async, - // to make this interruptable. + // to make this interruptible. if(asynch_ && requiresWakeUp() && prev_status == NodeStatus::IDLE && current_child_idx_ < children_count) { @@ -86,13 +87,15 @@ NodeStatus SequenceNode::tick() } // end while loop // The entire while loop completed. This means that all the children returned SUCCESS. + const bool all_children_skipped = (skipped_count_ == children_count); if(current_child_idx_ == children_count) { resetChildren(); current_child_idx_ = 0; + skipped_count_ = 0; } // Skip if ALL the nodes have been skipped - return (skipped_count_ == children_count) ? NodeStatus::SKIPPED : NodeStatus::SUCCESS; + return (all_children_skipped) ? NodeStatus::SKIPPED : NodeStatus::SUCCESS; } } // namespace BT diff --git a/src/controls/sequence_with_memory_node.cpp b/src/controls/sequence_with_memory_node.cpp index f47c798b6..3031de9b2 100644 --- a/src/controls/sequence_with_memory_node.cpp +++ b/src/controls/sequence_with_memory_node.cpp @@ -25,7 +25,7 @@ NodeStatus SequenceWithMemory::tick() { const size_t children_count = children_nodes_.size(); - if(status() == NodeStatus::IDLE) + if(!isStatusActive(status())) { skipped_count_ = 0; } @@ -55,7 +55,7 @@ NodeStatus SequenceWithMemory::tick() case NodeStatus::SUCCESS: { current_child_idx_++; // Return the execution flow if the child is async, - // to make this interruptable. + // to make this interruptible. if(requiresWakeUp() && prev_status == NodeStatus::IDLE && current_child_idx_ < children_count) { @@ -79,13 +79,15 @@ NodeStatus SequenceWithMemory::tick() } // end while loop // The entire while loop completed. This means that all the children returned SUCCESS. + const bool all_children_skipped = (skipped_count_ == children_count); if(current_child_idx_ == children_count) { resetChildren(); current_child_idx_ = 0; + skipped_count_ = 0; } // Skip if ALL the nodes have been skipped - return (skipped_count_ == children_count) ? NodeStatus::SKIPPED : NodeStatus::SUCCESS; + return (all_children_skipped) ? NodeStatus::SKIPPED : NodeStatus::SUCCESS; } void SequenceWithMemory::halt() diff --git a/src/decorators/repeat_node.cpp b/src/decorators/repeat_node.cpp index d6b6f7c68..9ea023ee1 100644 --- a/src/decorators/repeat_node.cpp +++ b/src/decorators/repeat_node.cpp @@ -59,7 +59,7 @@ NodeStatus RepeatNode::tick() resetChild(); // Return the execution flow if the child is async, - // to make this interruptable. + // to make this interruptible. if(requiresWakeUp() && prev_status == NodeStatus::IDLE && do_loop) { emitWakeUpSignal(); diff --git a/src/decorators/retry_node.cpp b/src/decorators/retry_node.cpp index 1a40247dd..d8c689c78 100644 --- a/src/decorators/retry_node.cpp +++ b/src/decorators/retry_node.cpp @@ -74,7 +74,7 @@ NodeStatus RetryNode::tick() resetChild(); // Return the execution flow if the child is async, - // to make this interruptable. + // to make this interruptible. if(requiresWakeUp() && prev_status == NodeStatus::IDLE && do_loop) { emitWakeUpSignal(); diff --git a/src/json_export.cpp b/src/json_export.cpp index d8ade7e65..6f95708ae 100644 --- a/src/json_export.cpp +++ b/src/json_export.cpp @@ -40,6 +40,22 @@ bool JsonExporter::toJson(const Any& any, nlohmann::json& dst) const dst[kTypeField] = "double"; dst[kValueField] = any.cast(); } + else if(type == typeid(std::vector)) + { + dst = any.cast>(); + } + else if(type == typeid(std::vector)) + { + dst = any.cast>(); + } + else if(type == typeid(std::vector)) + { + dst = any.cast>(); + } + else if(type == typeid(std::vector)) + { + dst = any.cast>(); + } else { auto it = to_json_converters_.find(type); @@ -61,7 +77,7 @@ JsonExporter::ExpectedEntry JsonExporter::fromJson(const nlohmann::json& source) { if(source.is_null()) { - return nonstd::make_unexpected("json object is null"); + return Entry{ BT::Any(), BT::TypeInfo::Create() }; } if(!source.contains(kTypeField)) { @@ -102,8 +118,8 @@ JsonExporter::ExpectedEntry JsonExporter::fromJson(const nlohmann::json& source) { return nonstd::make_unexpected("Type not found in registered list"); } - auto func_it = from_json_converters_.find(type_it->second.type()); - if(func_it == from_json_converters_.end()) + auto func_it = from_converters.find(type_it->second.type()); + if(func_it == from_converters.end()) { return nonstd::make_unexpected("Type not found in registered list"); } diff --git a/src/loggers/zmq.hpp b/src/loggers/zmq.hpp index e029b88d8..26c2c6f42 100644 --- a/src/loggers/zmq.hpp +++ b/src/loggers/zmq.hpp @@ -439,7 +439,7 @@ class message_t // NOTE this constructor will include the null terminator // when called with a string literal. // An overload taking const char* can not be added because - // it would be preferred over this function and break compatiblity. + // it would be preferred over this function and break compatibility. template ::value>::type> ZMQ_DEPRECATED("from 4.7.0, use constructors taking iterators, (pointer, size) " diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index 1f77eb3b0..cf45c568b 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -37,10 +37,6 @@ #include "tinyxml2/tinyxml2.h" #include -#ifdef USING_ROS -#include -#endif - #ifdef USING_ROS2 #include #endif @@ -284,9 +280,7 @@ void XMLParser::PImpl::loadDocImpl(XMLDocument* doc, bool add_includes) else { std::string ros_pkg_path; -#ifdef USING_ROS - ros_pkg_path = ros::package::getPath(ros_pkg_relative_path); -#elif defined USING_ROS2 +#if defined USING_ROS2 ros_pkg_path = ament_index_cpp::get_package_share_directory(ros_pkg_relative_path); #else diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 261c81f39..22e9ca7a2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -28,17 +28,13 @@ set(BT_TESTS gtest_tree.cpp gtest_updates.cpp gtest_wakeup.cpp + gtest_interface.cpp script_parser_test.cpp test_helper.hpp ) -set(TEST_DEPENDECIES - ${BTCPP_LIBRARY} - foonathan::lexy - bt_sample_nodes) - -if(ament_cmake_FOUND AND BUILD_TESTING) +if(ament_cmake_FOUND) find_package(ament_cmake_gmock REQUIRED) @@ -58,16 +54,17 @@ elseif(catkin_FOUND AND CATKIN_ENABLE_TESTING) else() find_package(GTest REQUIRED) - enable_testing() - add_executable(${BTCPP_LIBRARY}_test ${BT_TESTS}) + enable_testing() + add_executable(behaviortree_cpp_test ${BT_TESTS}) + add_test(NAME btcpp_test COMMAND behaviortree_cpp_test) - target_link_libraries(${PROJECT_NAME}_test - ${TEST_DEPENDECIES} - Threads::Threads - GTest::gtest) + target_link_libraries(behaviortree_cpp_test + GTest::gtest + GTest::gtest_main) endif() -target_include_directories(${BTCPP_LIBRARY}_test PRIVATE include ${PROJECT_SOURCE_DIR}/3rdparty) -target_compile_definitions(${BTCPP_LIBRARY}_test PRIVATE BT_TEST_FOLDER="${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(behaviortree_cpp_test ${BTCPP_LIBRARY} bt_sample_nodes foonathan::lexy) +target_include_directories(behaviortree_cpp_test PRIVATE include ${PROJECT_SOURCE_DIR}/3rdparty) +target_compile_definitions(behaviortree_cpp_test PRIVATE BT_TEST_FOLDER="${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/tests/gtest_async_action_node.cpp b/tests/gtest_async_action_node.cpp index 7e54384d0..fd19f05ee 100644 --- a/tests/gtest_async_action_node.cpp +++ b/tests/gtest_async_action_node.cpp @@ -70,7 +70,7 @@ TEST_P(NodeStatusFixture, normal_routine) TEST_F(MockedThreadedActionFixture, no_halt) { // Test verifies that halt returns immediately, if the node is idle. It - // further checks if the halt-flag is resetted correctly. + // further checks if the halt-flag is reset correctly. sn.halt(); ASSERT_TRUE(sn.isHaltRequested()); diff --git a/tests/gtest_blackboard.cpp b/tests/gtest_blackboard.cpp index fcbc272a3..35be37dab 100644 --- a/tests/gtest_blackboard.cpp +++ b/tests/gtest_blackboard.cpp @@ -458,6 +458,29 @@ struct Point double y; }; +// Template specialization to converts a string to Point. +namespace BT +{ +template <> +[[nodiscard]] Point convertFromString(StringView str) +{ + // We expect real numbers separated by semicolons + auto parts = splitString(str, ';'); + if(parts.size() != 2) + { + throw RuntimeError("invalid input)"); + } + else + { + Point output{ 0.0, 0.0 }; + output.x = convertFromString(parts[0]); + output.y = convertFromString(parts[1]); + // std::cout << "Building a position 2d object " << output.x << "; " << output.y << "\n" << std::flush; + return output; + } +} +} // end namespace BT + TEST(BlackboardTest, SetBlackboard_Issue725) { BT::BehaviorTreeFactory factory; @@ -654,3 +677,180 @@ TEST(BlackboardTest, TimestampedInterface) ASSERT_EQ(stamp_opt->seq, 2); ASSERT_GE(stamp_opt->time.count(), nsec_before); } + +TEST(BlackboardTest, SetBlackboard_Upd_Ts_SeqId) +{ + BT::BehaviorTreeFactory factory; + + const std::string xml_text = R"( + + + +