From 028c56732e3bc964ee58448a215b7adc17815bf3 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Wed, 25 Feb 2026 15:43:40 +0000 Subject: [PATCH 01/14] feat(dx): add test.sh convenience script with presets Provides named presets (unit, integ, lint, tidy, all) with parallel CTest execution to avoid remembering ctest filter flags. --- scripts/test.sh | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100755 scripts/test.sh diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 00000000..1b44a664 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# Copyright 2026 bburda +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +# Quick-test presets for ros2_medkit development. +# Usage: ./scripts/test.sh [preset] [extra colcon test args...] +# +# Presets: +# unit - Unit tests only, no linters, no integration (DEFAULT) +# integ - Integration tests only +# lint - Linters only (clang-format, copyright, cmake-lint; NO clang-tidy) +# tidy - clang-tidy only (slow, ~8-10 min) +# all - Everything (equivalent to bare colcon test) +# - Run a single test by CTest name regex +# +# Examples: +# ./scripts/test.sh # unit tests only +# ./scripts/test.sh integ # integration tests only +# ./scripts/test.sh lint # fast linters only +# ./scripts/test.sh test_health_handler # single test +# ./scripts/test.sh unit --packages-select ros2_medkit_gateway + +PRESET="${1:-unit}" +shift 2>/dev/null || true + +COMMON_ARGS=(--event-handlers console_direct+ --parallel-workers "$(nproc)" --return-code-on-test-failure) + +# Run tests, capture exit code so we always show results even on failure. +set +e +case "$PRESET" in + unit) + echo "==> Running unit tests (no linters, no integration)" + colcon test "${COMMON_ARGS[@]}" \ + --ctest-args -j "$(nproc)" -LE "linter|integration" \ + "$@" + ;; + integ) + echo "==> Running integration tests only" + colcon test "${COMMON_ARGS[@]}" \ + --ctest-args -j "$(nproc)" -L integration \ + "$@" + ;; + lint) + echo "==> Running linters (excluding clang-tidy)" + colcon test "${COMMON_ARGS[@]}" \ + --ctest-args -j "$(nproc)" -L linter -E "clang_tidy" \ + "$@" + ;; + tidy) + echo "==> Running clang-tidy (this will take a while)" + colcon test "${COMMON_ARGS[@]}" \ + --ctest-args -j "$(nproc)" -R "clang_tidy" \ + "$@" + ;; + all) + echo "==> Running all tests" + colcon test "${COMMON_ARGS[@]}" \ + --ctest-args -j "$(nproc)" \ + "$@" + ;; + *) + echo "==> Running tests matching: $PRESET" + colcon test "${COMMON_ARGS[@]}" \ + --ctest-args -j "$(nproc)" -R "$PRESET" \ + "$@" + ;; +esac +TEST_EXIT=$? +set -e + +echo "" +echo "==> Results:" +colcon test-result --verbose || true +exit "$TEST_EXIT" From 53f376791c1d8a60e360cbfbee5ed5de148b1cce Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Wed, 25 Feb 2026 15:45:06 +0000 Subject: [PATCH 02/14] perf(build): make clang-tidy opt-in locally, mandatory in CI clang-tidy takes 8-10 min on the gateway package alone. Gated behind ENABLE_CLANG_TIDY=OFF (default). CI sets it ON for the Jazzy linter job. Developers can still run it with: colcon build --cmake-args -DENABLE_CLANG_TIDY=ON ./scripts/test.sh tidy --- .github/workflows/ci.yml | 6 ++++- .../CMakeLists.txt | 13 ++++++++++- src/ros2_medkit_fault_manager/CMakeLists.txt | 13 ++++++++++- src/ros2_medkit_fault_reporter/CMakeLists.txt | 13 ++++++++++- src/ros2_medkit_gateway/CMakeLists.txt | 22 ++++++++++++------- src/ros2_medkit_serialization/CMakeLists.txt | 20 ++++++++++------- 6 files changed, 67 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 973eea61..cb99320d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,8 +75,12 @@ jobs: - name: Build packages run: | source /opt/ros/${{ matrix.ros_distro }}/setup.bash + CMAKE_EXTRA_ARGS="" + if [ "${{ matrix.ros_distro }}" = "jazzy" ]; then + CMAKE_EXTRA_ARGS="-DENABLE_CLANG_TIDY=ON" + fi colcon build --symlink-install \ - --cmake-args -DCMAKE_BUILD_TYPE=Release \ + --cmake-args -DCMAKE_BUILD_TYPE=Release ${CMAKE_EXTRA_ARGS} \ --event-handlers console_direct+ - name: Run linters (clang-format, clang-tidy, etc.) diff --git a/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt b/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt index 3899734d..4746741a 100644 --- a/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt +++ b/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt @@ -91,6 +91,8 @@ install(DIRECTORY launch config ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET) ament_export_dependencies(rclcpp diagnostic_msgs ros2_medkit_msgs ros2_medkit_fault_reporter) +option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) + # Testing if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) @@ -99,9 +101,18 @@ if(BUILD_TESTING) set(ament_cmake_clang_format_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-format") set(ament_cmake_clang_tidy_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") - list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint) + list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint ament_cmake_clang_tidy) ament_lint_auto_find_test_dependencies() + if(ENABLE_CLANG_TIDY) + find_package(ament_cmake_clang_tidy REQUIRED) + set(_clang_tidy_config "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") + ament_clang_tidy( + "${CMAKE_CURRENT_BINARY_DIR}" + CONFIG_FILE "${_clang_tidy_config}" + ) + endif() + # Unit tests ament_add_gtest(test_diagnostic_bridge test/test_diagnostic_bridge.cpp) target_link_libraries(test_diagnostic_bridge diagnostic_bridge_lib) diff --git a/src/ros2_medkit_fault_manager/CMakeLists.txt b/src/ros2_medkit_fault_manager/CMakeLists.txt index 6b49ce5d..bf4e30f4 100644 --- a/src/ros2_medkit_fault_manager/CMakeLists.txt +++ b/src/ros2_medkit_fault_manager/CMakeLists.txt @@ -111,6 +111,8 @@ install(DIRECTORY launch config DESTINATION share/${PROJECT_NAME} ) +option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) + # Testing if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) @@ -119,9 +121,18 @@ if(BUILD_TESTING) set(ament_cmake_clang_format_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-format") set(ament_cmake_clang_tidy_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") - list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint) + list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint ament_cmake_clang_tidy) ament_lint_auto_find_test_dependencies() + if(ENABLE_CLANG_TIDY) + find_package(ament_cmake_clang_tidy REQUIRED) + set(_clang_tidy_config "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") + ament_clang_tidy( + "${CMAKE_CURRENT_BINARY_DIR}" + CONFIG_FILE "${_clang_tidy_config}" + ) + endif() + # Unit tests ament_add_gtest(test_fault_manager test/test_fault_manager.cpp) target_link_libraries(test_fault_manager fault_manager_lib) diff --git a/src/ros2_medkit_fault_reporter/CMakeLists.txt b/src/ros2_medkit_fault_reporter/CMakeLists.txt index 66debb79..923297ab 100644 --- a/src/ros2_medkit_fault_reporter/CMakeLists.txt +++ b/src/ros2_medkit_fault_reporter/CMakeLists.txt @@ -78,6 +78,8 @@ install(DIRECTORY include/ ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET) ament_export_dependencies(rclcpp ros2_medkit_msgs) +option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) + # Testing if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) @@ -86,9 +88,18 @@ if(BUILD_TESTING) set(ament_cmake_clang_format_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-format") set(ament_cmake_clang_tidy_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") - list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint) + list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint ament_cmake_clang_tidy) ament_lint_auto_find_test_dependencies() + if(ENABLE_CLANG_TIDY) + find_package(ament_cmake_clang_tidy REQUIRED) + set(_clang_tidy_config "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") + ament_clang_tidy( + "${CMAKE_CURRENT_BINARY_DIR}" + CONFIG_FILE "${_clang_tidy_config}" + ) + endif() + # Unit tests ament_add_gtest(test_local_filter test/test_local_filter.cpp) target_link_libraries(test_local_filter fault_reporter_lib) diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index 3010a408..60b6040d 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -188,6 +188,10 @@ install(PROGRAMS scripts/get_type_schema.py scripts/generate_dev_certs.sh DESTINATION share/${PROJECT_NAME}/scripts ) +# clang-tidy: opt-in locally (slow, ~8-10 min), mandatory in CI. +# Enable with: colcon build --cmake-args -DENABLE_CLANG_TIDY=ON +option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) + # Testing if(BUILD_TESTING) # Linting and code quality checks @@ -244,14 +248,16 @@ if(BUILD_TESTING) CONFIG_FILE "${ament_cmake_clang_format_CONFIG_FILE}" ) - # Configure clang-tidy manually with increased timeout (1500s instead of default 300s) - # This is needed because the project has many files and clang-tidy analysis takes time - ament_clang_tidy( - "${CMAKE_CURRENT_BINARY_DIR}" - CONFIG_FILE "${ament_cmake_clang_tidy_CONFIG_FILE}" - HEADER_FILTER "${ament_cmake_clang_tidy_HEADER_FILTER}" - TIMEOUT 1500 - ) + # clang-tidy: gated behind ENABLE_CLANG_TIDY (default OFF). + # CI sets -DENABLE_CLANG_TIDY=ON on Jazzy. Timeout 1500s for 142-file package. + if(ENABLE_CLANG_TIDY) + ament_clang_tidy( + "${CMAKE_CURRENT_BINARY_DIR}" + CONFIG_FILE "${ament_cmake_clang_tidy_CONFIG_FILE}" + HEADER_FILTER "${ament_cmake_clang_tidy_HEADER_FILTER}" + TIMEOUT 1500 + ) + endif() # Add GTest find_package(ament_cmake_gtest REQUIRED) diff --git a/src/ros2_medkit_serialization/CMakeLists.txt b/src/ros2_medkit_serialization/CMakeLists.txt index 654d49e4..d304b8be 100644 --- a/src/ros2_medkit_serialization/CMakeLists.txt +++ b/src/ros2_medkit_serialization/CMakeLists.txt @@ -102,6 +102,8 @@ install(DIRECTORY include/ DESTINATION include ) +option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) + if(BUILD_TESTING) find_package(ament_cmake_gtest REQUIRED) find_package(ament_lint_auto REQUIRED) @@ -140,14 +142,16 @@ if(BUILD_TESTING) "test/test_type_cache.cpp" ) - # Configure clang-tidy manually for non-vendored files only - set(_clang_tidy_config "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") - ament_clang_tidy( - "${CMAKE_CURRENT_BINARY_DIR}" - CONFIG_FILE "${_clang_tidy_config}" - HEADER_FILTER "^${CMAKE_CURRENT_SOURCE_DIR}/include/ros2_medkit_serialization/[^v].*" - TIMEOUT 300 - ) + # clang-tidy: gated behind ENABLE_CLANG_TIDY (default OFF). + if(ENABLE_CLANG_TIDY) + set(_clang_tidy_config "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") + ament_clang_tidy( + "${CMAKE_CURRENT_BINARY_DIR}" + CONFIG_FILE "${_clang_tidy_config}" + HEADER_FILTER "^${CMAKE_CURRENT_SOURCE_DIR}/include/ros2_medkit_serialization/[^v].*" + TIMEOUT 300 + ) + endif() # Unit tests ament_add_gtest(test_type_cache test/test_type_cache.cpp) From 70176f6933a8de89347ac5070c635a4db659df29 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Wed, 25 Feb 2026 15:47:34 +0000 Subject: [PATCH 03/14] chore: fix copyright attribution to bburda --- src/ros2_medkit_diagnostic_bridge/CMakeLists.txt | 2 +- src/ros2_medkit_fault_manager/CMakeLists.txt | 3 ++- src/ros2_medkit_fault_reporter/CMakeLists.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt b/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt index 4746741a..e7af5626 100644 --- a/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt +++ b/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2026 mfaferek93 +# Copyright 2026 mfaferek93, bburda # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/src/ros2_medkit_fault_manager/CMakeLists.txt b/src/ros2_medkit_fault_manager/CMakeLists.txt index bf4e30f4..9086539e 100644 --- a/src/ros2_medkit_fault_manager/CMakeLists.txt +++ b/src/ros2_medkit_fault_manager/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2025 mfaferek93 +# Copyright 2025-2026 mfaferek93, bburda # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Multi-distro compatibility (Humble / Jazzy / Rolling) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake") include(ROS2MedkitCompat) +include(ROS2MedkitCcache) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion) diff --git a/src/ros2_medkit_fault_reporter/CMakeLists.txt b/src/ros2_medkit_fault_reporter/CMakeLists.txt index 923297ab..25761b5a 100644 --- a/src/ros2_medkit_fault_reporter/CMakeLists.txt +++ b/src/ros2_medkit_fault_reporter/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2025 mfaferek93 +# Copyright 2025-2026 mfaferek93, bburda # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 8e56f5a9c2d9b011711cd1ff790c50727cdac19e Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Wed, 25 Feb 2026 15:47:52 +0000 Subject: [PATCH 04/14] feat(build): auto-detect ccache for faster incremental rebuilds Adds ROS2MedkitCcache.cmake module that enables ccache when found on PATH. All C++ packages include it. No effect when ccache is absent. Set CCACHE_SLOPPINESS=pch_defines,time_macros for PCH compatibility. --- cmake/ROS2MedkitCcache.cmake | 36 +++++++++++++++++++ .../CMakeLists.txt | 3 ++ src/ros2_medkit_fault_reporter/CMakeLists.txt | 3 ++ src/ros2_medkit_gateway/CMakeLists.txt | 1 + src/ros2_medkit_serialization/CMakeLists.txt | 1 + 5 files changed, 44 insertions(+) create mode 100644 cmake/ROS2MedkitCcache.cmake diff --git a/cmake/ROS2MedkitCcache.cmake b/cmake/ROS2MedkitCcache.cmake new file mode 100644 index 00000000..589f05af --- /dev/null +++ b/cmake/ROS2MedkitCcache.cmake @@ -0,0 +1,36 @@ +# Copyright 2026 bburda +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Auto-detect and enable ccache if available. +# Include this module early in CMakeLists.txt (before add_library/add_executable). +# +# Override with: -DCMAKE_CXX_COMPILER_LAUNCHER= / -DCMAKE_C_COMPILER_LAUNCHER= +# +# When using with precompiled headers (PCH), set the environment variable: +# export CCACHE_SLOPPINESS=pch_defines,time_macros +# Without this, ccache will have poor hit rates on PCH-using targets. + +if(NOT CMAKE_C_COMPILER_LAUNCHER OR NOT CMAKE_CXX_COMPILER_LAUNCHER) + find_program(_CCACHE ccache) + if(_CCACHE) + if(NOT CMAKE_C_COMPILER_LAUNCHER) + set(CMAKE_C_COMPILER_LAUNCHER "${_CCACHE}" CACHE STRING "C compiler launcher") + endif() + if(NOT CMAKE_CXX_COMPILER_LAUNCHER) + set(CMAKE_CXX_COMPILER_LAUNCHER "${_CCACHE}" CACHE STRING "C++ compiler launcher") + endif() + message(STATUS "ccache found: ${_CCACHE}") + endif() + unset(_CCACHE) +endif() diff --git a/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt b/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt index e7af5626..ee772cd6 100644 --- a/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt +++ b/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt @@ -19,6 +19,9 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake") +include(ROS2MedkitCcache) + if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion) endif() diff --git a/src/ros2_medkit_fault_reporter/CMakeLists.txt b/src/ros2_medkit_fault_reporter/CMakeLists.txt index 25761b5a..16dddf63 100644 --- a/src/ros2_medkit_fault_reporter/CMakeLists.txt +++ b/src/ros2_medkit_fault_reporter/CMakeLists.txt @@ -19,6 +19,9 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake") +include(ROS2MedkitCcache) + if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion) endif() diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index 60b6040d..dee3eac4 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -8,6 +8,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Multi-distro compatibility (Humble / Jazzy / Rolling) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake") include(ROS2MedkitCompat) +include(ROS2MedkitCcache) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion) diff --git a/src/ros2_medkit_serialization/CMakeLists.txt b/src/ros2_medkit_serialization/CMakeLists.txt index d304b8be..5e099b5a 100644 --- a/src/ros2_medkit_serialization/CMakeLists.txt +++ b/src/ros2_medkit_serialization/CMakeLists.txt @@ -8,6 +8,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Multi-distro compatibility (Humble / Jazzy / Rolling) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake") include(ROS2MedkitCompat) +include(ROS2MedkitCcache) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) From d557fd0faa9b57dafec228e609b2402aead8cfce Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Wed, 25 Feb 2026 15:48:49 +0000 Subject: [PATCH 05/14] perf(build): add precompiled headers for gateway package PCH for rclcpp, nlohmann/json, httplib, and common STL headers. Reduces full gateway build time by ~30-40%. Can be disabled with -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON. --- src/ros2_medkit_gateway/CMakeLists.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index dee3eac4..2f7e74ab 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -163,6 +163,25 @@ target_link_libraries(gateway_lib ${CMAKE_DL_LIBS} ) +# Precompiled headers - heavy headers used across most translation units. +# Speeds up full builds by ~30-40%. Disable with -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON. +target_precompile_headers(gateway_lib PRIVATE + + + + + + + + + + + + + + +) + # Gateway node executable add_executable(gateway_node src/main.cpp) target_link_libraries(gateway_node gateway_lib) From d04f7a1a93bf06fb83199137bc223ab9d4e3b481 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Wed, 25 Feb 2026 15:49:19 +0000 Subject: [PATCH 06/14] perf(ci): add ccache with GitHub Actions cache ccache with persistent cache reduces CI rebuild times by ~30-50% on subsequent runs. CCACHE_SLOPPINESS set for PCH compatibility. --- .github/workflows/ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb99320d..2d4648fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,17 @@ jobs: with: required-ros-distributions: ${{ matrix.ros_distro }} + - name: Install ccache + run: apt-get install -y ccache + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: /root/.cache/ccache + key: ccache-${{ matrix.ros_distro }}-${{ github.sha }} + restore-keys: | + ccache-${{ matrix.ros_distro }}- + - name: Install cpp-httplib from source (Humble) if: matrix.ros_distro == 'humble' run: | @@ -73,6 +84,10 @@ jobs: fi - name: Build packages + env: + CCACHE_DIR: /root/.cache/ccache + CCACHE_MAXSIZE: 500M + CCACHE_SLOPPINESS: pch_defines,time_macros run: | source /opt/ros/${{ matrix.ros_distro }}/setup.bash CMAKE_EXTRA_ARGS="" @@ -82,6 +97,7 @@ jobs: colcon build --symlink-install \ --cmake-args -DCMAKE_BUILD_TYPE=Release ${CMAKE_EXTRA_ARGS} \ --event-handlers console_direct+ + ccache -s - name: Run linters (clang-format, clang-tidy, etc.) if: matrix.ros_distro == 'jazzy' From f654fcca76fca3fdaf288a0cc4ded44ad3e7cde9 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Wed, 25 Feb 2026 15:52:21 +0000 Subject: [PATCH 07/14] feat(dx): add incremental clang-tidy as pre-push hook Runs clang-tidy only on changed C++ source files using a merged compile_commands.json. Typical run: 5-30s vs 8-10 min for full analysis. Registered as pre-push hook (not pre-commit) to avoid slowing normal commits. Run ./scripts/merge-compile-commands.sh after build. --- .devcontainer/setup-env.sh | 6 +++ .pre-commit-config.yaml | 12 ++++++ scripts/clang-tidy-diff.sh | 66 +++++++++++++++++++++++++++++++ scripts/merge-compile-commands.sh | 55 ++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100755 scripts/clang-tidy-diff.sh create mode 100755 scripts/merge-compile-commands.sh diff --git a/.devcontainer/setup-env.sh b/.devcontainer/setup-env.sh index b0774967..f2a524fd 100755 --- a/.devcontainer/setup-env.sh +++ b/.devcontainer/setup-env.sh @@ -22,6 +22,12 @@ if [ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]; then fi rosdep update +# Install pre-commit hooks (pre-commit for local, pre-push for clang-tidy) +if command -v pre-commit &>/dev/null; then + pre-commit install + pre-commit install --hook-type pre-push +fi + # Test installations echo "Testing installations..." echo "ROS2 Jazzy: $(test -f /opt/ros/jazzy/setup.bash && echo 'Installed' || echo 'Not found')" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a243c88..80693442 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -60,3 +60,15 @@ repos: language: system types_or: [c, c++, python, cmake] exclude: /vendored/ + + # ── Incremental clang-tidy (pre-push only) ──────────────────────── + # Requires: pre-commit install --hook-type pre-push + - repo: local + hooks: + - id: clang-tidy-diff + name: clang-tidy (changed files only) + entry: ./scripts/clang-tidy-diff.sh + language: system + types: [c++] + exclude: /vendored/ + stages: [pre-push] diff --git a/scripts/clang-tidy-diff.sh b/scripts/clang-tidy-diff.sh new file mode 100755 index 00000000..3221ba24 --- /dev/null +++ b/scripts/clang-tidy-diff.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Copyright 2026 bburda +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Incremental clang-tidy: only analyses C++ source files passed as arguments. +# Designed for pre-commit which passes staged filenames. +# Requires: merged compile_commands.json (run ./scripts/merge-compile-commands.sh after build). +# +# Usage (standalone): +# ./scripts/clang-tidy-diff.sh src/ros2_medkit_gateway/src/config.cpp +# +# Usage (via pre-commit): automatic - pre-commit passes changed files. + +set -euo pipefail + +COMPILE_DB="build/compile_commands.json" + +if [ ! -f "$COMPILE_DB" ]; then + echo "Warning: No merged compile_commands.json found." + echo "Run: colcon build && ./scripts/merge-compile-commands.sh" + exit 0 +fi + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CLANG_TIDY_CONFIG=".clang-tidy" +ERRORS=0 + +for file in "$@"; do + # Only process C++ source files (.cpp). Headers are checked transitively + # through their including .cpp files. CI runs full clang-tidy on all files. + case "$file" in + *.cpp) ;; + *) continue ;; + esac + + if [[ "$file" == *"/vendored/"* ]]; then + continue + fi + + # Resolve to absolute path for matching against compile_commands.json + ABS_FILE="$REPO_ROOT/$file" + if [ ! -f "$ABS_FILE" ]; then + ABS_FILE="$(realpath "$file" 2>/dev/null || echo "$file")" + fi + + echo "clang-tidy: $file" + if ! clang-tidy -p "$(dirname "$COMPILE_DB")" --config-file="$CLANG_TIDY_CONFIG" "$ABS_FILE"; then + ERRORS=$((ERRORS + 1)) + fi +done + +if [ "$ERRORS" -gt 0 ]; then + echo "clang-tidy found issues in $ERRORS file(s)" + exit 1 +fi diff --git a/scripts/merge-compile-commands.sh b/scripts/merge-compile-commands.sh new file mode 100755 index 00000000..697f5e4a --- /dev/null +++ b/scripts/merge-compile-commands.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Copyright 2026 bburda +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Merges per-package compile_commands.json files into a single database. +# Run after colcon build to enable project-wide clang-tidy analysis. +# +# Usage: ./scripts/merge-compile-commands.sh +# Output: build/compile_commands.json + +set -euo pipefail + +MERGED="build/compile_commands.json" + +# Find all per-package compile_commands.json files +DATABASES=(build/*/compile_commands.json) + +if [ ${#DATABASES[@]} -eq 0 ] || [ ! -f "${DATABASES[0]}" ]; then + echo "Error: No compile_commands.json found in build/*/." + echo "Run 'colcon build' first." + exit 1 +fi + +# Merge using jq if available, otherwise use Python +if command -v jq &>/dev/null; then + jq -s 'add' "${DATABASES[@]}" > "$MERGED" +else + python3 -c " +import json, sys +entries = [] +for f in sys.argv[2:]: + with open(f) as fh: + entries.extend(json.load(fh)) +with open(sys.argv[1], 'w') as fh: + json.dump(entries, fh, indent=2) +" "$MERGED" "${DATABASES[@]}" +fi + +if command -v jq &>/dev/null; then + COUNT=$(jq length "$MERGED") +else + COUNT=$(python3 -c "import json; print(len(json.load(open('$MERGED'))))" 2>/dev/null || echo '?') +fi +echo "Merged ${#DATABASES[@]} databases into $MERGED ($COUNT entries)" From 112f30dfeffbf3497f960e66a29ce9d0b2a173fc Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Wed, 25 Feb 2026 17:40:33 +0000 Subject: [PATCH 08/14] feat(devcontainer): add ccache with PCH-compatible sloppiness config --- .devcontainer/Dockerfile | 1 + .devcontainer/setup-env.sh | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 41090d7b..8e0b9220 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -34,6 +34,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y \ yq \ ca-certificates \ clang-format \ + ccache \ clang-tidy \ graphviz \ plantuml \ diff --git a/.devcontainer/setup-env.sh b/.devcontainer/setup-env.sh index f2a524fd..36b26c4f 100755 --- a/.devcontainer/setup-env.sh +++ b/.devcontainer/setup-env.sh @@ -5,6 +5,10 @@ set -e grep -Fxq "source /opt/ros/jazzy/setup.bash" ~/.bashrc || echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc grep -Fxq "source /opt/ros/jazzy/setup.zsh" ~/.zshrc || echo "source /opt/ros/jazzy/setup.zsh" >> ~/.zshrc +# ccache configuration for PCH compatibility +grep -Fxq 'export CCACHE_SLOPPINESS=pch_defines,time_macros' ~/.bashrc || echo 'export CCACHE_SLOPPINESS=pch_defines,time_macros' >> ~/.bashrc +grep -Fxq 'export CCACHE_SLOPPINESS=pch_defines,time_macros' ~/.zshrc || echo 'export CCACHE_SLOPPINESS=pch_defines,time_macros' >> ~/.zshrc + # Source the current shell to apply changes immediately if [ -n "$ZSH_VERSION" ]; then # Running in zsh From 53f85b1d47703b0f4d135cf8294a5b32dbdb8a26 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Wed, 25 Feb 2026 19:52:54 +0000 Subject: [PATCH 09/14] refactor(cmake): centralize ENABLE_CLANG_TIDY into ROS2MedkitLinting.cmake Extract duplicated option(ENABLE_CLANG_TIDY) and clang-tidy configuration into a shared cmake module. Each package now calls ros2_medkit_clang_tidy() with optional HEADER_FILTER and TIMEOUT parameters. --- cmake/ROS2MedkitLinting.cmake | 48 +++++++++++++++++++ .../CMakeLists.txt | 13 +---- src/ros2_medkit_fault_manager/CMakeLists.txt | 13 +---- src/ros2_medkit_fault_reporter/CMakeLists.txt | 13 +---- src/ros2_medkit_gateway/CMakeLists.txt | 27 +++-------- src/ros2_medkit_serialization/CMakeLists.txt | 18 ++----- 6 files changed, 65 insertions(+), 67 deletions(-) create mode 100644 cmake/ROS2MedkitLinting.cmake diff --git a/cmake/ROS2MedkitLinting.cmake b/cmake/ROS2MedkitLinting.cmake new file mode 100644 index 00000000..4db1474c --- /dev/null +++ b/cmake/ROS2MedkitLinting.cmake @@ -0,0 +1,48 @@ +# Copyright 2026 bburda +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Shared linting configuration for ros2_medkit packages. +# Include this in CMakeLists.txt BEFORE the if(BUILD_TESTING) block +# (alongside include(ROS2MedkitCcache) - CMAKE_MODULE_PATH is already set). +# +# Provides: +# option ENABLE_CLANG_TIDY (default OFF) +# function ros2_medkit_clang_tidy([HEADER_FILTER ] [TIMEOUT ]) + +option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) + +# Capture at include-time: inside a function CMAKE_CURRENT_LIST_DIR resolves to the caller. +set(_ROS2_MEDKIT_CLANG_TIDY_CONFIG "${CMAKE_CURRENT_LIST_DIR}/../.clang-tidy") + +function(ros2_medkit_clang_tidy) + if(NOT ENABLE_CLANG_TIDY) + return() + endif() + + cmake_parse_arguments(ARG "" "HEADER_FILTER;TIMEOUT" "" ${ARGN}) + + find_package(ament_cmake_clang_tidy REQUIRED) + + set(_args "${CMAKE_CURRENT_BINARY_DIR}" CONFIG_FILE "${_ROS2_MEDKIT_CLANG_TIDY_CONFIG}") + + if(ARG_HEADER_FILTER) + list(APPEND _args HEADER_FILTER "${ARG_HEADER_FILTER}") + endif() + + if(ARG_TIMEOUT) + list(APPEND _args TIMEOUT "${ARG_TIMEOUT}") + endif() + + ament_clang_tidy(${_args}) +endfunction() diff --git a/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt b/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt index ee772cd6..f392b14f 100644 --- a/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt +++ b/src/ros2_medkit_diagnostic_bridge/CMakeLists.txt @@ -21,6 +21,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake") include(ROS2MedkitCcache) +include(ROS2MedkitLinting) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion) @@ -94,8 +95,6 @@ install(DIRECTORY launch config ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET) ament_export_dependencies(rclcpp diagnostic_msgs ros2_medkit_msgs ros2_medkit_fault_reporter) -option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) - # Testing if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) @@ -103,18 +102,10 @@ if(BUILD_TESTING) find_package(launch_testing_ament_cmake REQUIRED) set(ament_cmake_clang_format_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-format") - set(ament_cmake_clang_tidy_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint ament_cmake_clang_tidy) ament_lint_auto_find_test_dependencies() - if(ENABLE_CLANG_TIDY) - find_package(ament_cmake_clang_tidy REQUIRED) - set(_clang_tidy_config "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") - ament_clang_tidy( - "${CMAKE_CURRENT_BINARY_DIR}" - CONFIG_FILE "${_clang_tidy_config}" - ) - endif() + ros2_medkit_clang_tidy() # Unit tests ament_add_gtest(test_diagnostic_bridge test/test_diagnostic_bridge.cpp) diff --git a/src/ros2_medkit_fault_manager/CMakeLists.txt b/src/ros2_medkit_fault_manager/CMakeLists.txt index 9086539e..7b7e3329 100644 --- a/src/ros2_medkit_fault_manager/CMakeLists.txt +++ b/src/ros2_medkit_fault_manager/CMakeLists.txt @@ -23,6 +23,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake") include(ROS2MedkitCompat) include(ROS2MedkitCcache) +include(ROS2MedkitLinting) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion) @@ -112,8 +113,6 @@ install(DIRECTORY launch config DESTINATION share/${PROJECT_NAME} ) -option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) - # Testing if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) @@ -121,18 +120,10 @@ if(BUILD_TESTING) find_package(launch_testing_ament_cmake REQUIRED) set(ament_cmake_clang_format_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-format") - set(ament_cmake_clang_tidy_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint ament_cmake_clang_tidy) ament_lint_auto_find_test_dependencies() - if(ENABLE_CLANG_TIDY) - find_package(ament_cmake_clang_tidy REQUIRED) - set(_clang_tidy_config "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") - ament_clang_tidy( - "${CMAKE_CURRENT_BINARY_DIR}" - CONFIG_FILE "${_clang_tidy_config}" - ) - endif() + ros2_medkit_clang_tidy() # Unit tests ament_add_gtest(test_fault_manager test/test_fault_manager.cpp) diff --git a/src/ros2_medkit_fault_reporter/CMakeLists.txt b/src/ros2_medkit_fault_reporter/CMakeLists.txt index 16dddf63..3d7fc61f 100644 --- a/src/ros2_medkit_fault_reporter/CMakeLists.txt +++ b/src/ros2_medkit_fault_reporter/CMakeLists.txt @@ -21,6 +21,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake") include(ROS2MedkitCcache) +include(ROS2MedkitLinting) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion) @@ -81,8 +82,6 @@ install(DIRECTORY include/ ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET) ament_export_dependencies(rclcpp ros2_medkit_msgs) -option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) - # Testing if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) @@ -90,18 +89,10 @@ if(BUILD_TESTING) find_package(launch_testing_ament_cmake REQUIRED) set(ament_cmake_clang_format_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-format") - set(ament_cmake_clang_tidy_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint ament_cmake_clang_tidy) ament_lint_auto_find_test_dependencies() - if(ENABLE_CLANG_TIDY) - find_package(ament_cmake_clang_tidy REQUIRED) - set(_clang_tidy_config "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") - ament_clang_tidy( - "${CMAKE_CURRENT_BINARY_DIR}" - CONFIG_FILE "${_clang_tidy_config}" - ) - endif() + ros2_medkit_clang_tidy() # Unit tests ament_add_gtest(test_local_filter test/test_local_filter.cpp) diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index 2f7e74ab..9be63260 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -9,6 +9,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake") include(ROS2MedkitCompat) include(ROS2MedkitCcache) +include(ROS2MedkitLinting) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion) @@ -208,22 +209,12 @@ install(PROGRAMS scripts/get_type_schema.py scripts/generate_dev_certs.sh DESTINATION share/${PROJECT_NAME}/scripts ) -# clang-tidy: opt-in locally (slow, ~8-10 min), mandatory in CI. -# Enable with: colcon build --cmake-args -DENABLE_CLANG_TIDY=ON -option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) - # Testing if(BUILD_TESTING) # Linting and code quality checks find_package(ament_lint_auto REQUIRED) - find_package(ament_cmake_clang_tidy REQUIRED) - - # Use custom clang-format and clang-tidy configs from repo root + # Use custom clang-format config from repo root set(ament_cmake_clang_format_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-format") - set(ament_cmake_clang_tidy_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") - - # Limit clang-tidy to only report issues from our source files (not vendored deps) - set(ament_cmake_clang_tidy_HEADER_FILTER "^${CMAKE_CURRENT_SOURCE_DIR}/(include|src|test)/") # Exclude linters that don't work well with vendored dependencies: # - uncrustify/cpplint: conflicts with clang-format @@ -268,16 +259,10 @@ if(BUILD_TESTING) CONFIG_FILE "${ament_cmake_clang_format_CONFIG_FILE}" ) - # clang-tidy: gated behind ENABLE_CLANG_TIDY (default OFF). - # CI sets -DENABLE_CLANG_TIDY=ON on Jazzy. Timeout 1500s for 142-file package. - if(ENABLE_CLANG_TIDY) - ament_clang_tidy( - "${CMAKE_CURRENT_BINARY_DIR}" - CONFIG_FILE "${ament_cmake_clang_tidy_CONFIG_FILE}" - HEADER_FILTER "${ament_cmake_clang_tidy_HEADER_FILTER}" - TIMEOUT 1500 - ) - endif() + ros2_medkit_clang_tidy( + HEADER_FILTER "^${CMAKE_CURRENT_SOURCE_DIR}/(include|src|test)/" + TIMEOUT 1500 + ) # Add GTest find_package(ament_cmake_gtest REQUIRED) diff --git a/src/ros2_medkit_serialization/CMakeLists.txt b/src/ros2_medkit_serialization/CMakeLists.txt index 5e099b5a..47f4b5c2 100644 --- a/src/ros2_medkit_serialization/CMakeLists.txt +++ b/src/ros2_medkit_serialization/CMakeLists.txt @@ -9,6 +9,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake") include(ROS2MedkitCompat) include(ROS2MedkitCcache) +include(ROS2MedkitLinting) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) @@ -103,12 +104,9 @@ install(DIRECTORY include/ DESTINATION include ) -option(ENABLE_CLANG_TIDY "Register clang-tidy as a CTest target" OFF) - if(BUILD_TESTING) find_package(ament_cmake_gtest REQUIRED) find_package(ament_lint_auto REQUIRED) - find_package(ament_cmake_clang_tidy REQUIRED) find_package(std_msgs REQUIRED) find_package(std_srvs REQUIRED) find_package(geometry_msgs REQUIRED) @@ -143,16 +141,10 @@ if(BUILD_TESTING) "test/test_type_cache.cpp" ) - # clang-tidy: gated behind ENABLE_CLANG_TIDY (default OFF). - if(ENABLE_CLANG_TIDY) - set(_clang_tidy_config "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy") - ament_clang_tidy( - "${CMAKE_CURRENT_BINARY_DIR}" - CONFIG_FILE "${_clang_tidy_config}" - HEADER_FILTER "^${CMAKE_CURRENT_SOURCE_DIR}/include/ros2_medkit_serialization/[^v].*" - TIMEOUT 300 - ) - endif() + ros2_medkit_clang_tidy( + HEADER_FILTER "^${CMAKE_CURRENT_SOURCE_DIR}/include/ros2_medkit_serialization/[^v].*" + TIMEOUT 300 + ) # Unit tests ament_add_gtest(test_type_cache test/test_type_cache.cpp) From 136a0161570df42dcd1f85fa94458b54491b8d4d Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 27 Feb 2026 08:25:37 +0100 Subject: [PATCH 10/14] perf(ci): add ccache to coverage job Adds ccache with persistent GitHub Actions cache to the coverage build. Uses separate cache key (ccache-coverage-) from Release builds since coverage builds with -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON. --- .github/workflows/ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d4648fb..f53932f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,6 +153,17 @@ jobs: with: required-ros-distributions: jazzy + - name: Install ccache + run: apt-get install -y ccache + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: /root/.cache/ccache + key: ccache-coverage-${{ github.sha }} + restore-keys: | + ccache-coverage- + - name: Install dependencies run: | apt-get update @@ -162,11 +173,16 @@ jobs: rosdep install --from-paths src --ignore-src -r -y - name: Build packages with coverage + env: + CCACHE_DIR: /root/.cache/ccache + CCACHE_MAXSIZE: 500M + CCACHE_SLOPPINESS: pch_defines,time_macros run: | source /opt/ros/jazzy/setup.bash colcon build --symlink-install \ --cmake-args -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON \ --event-handlers console_direct+ + ccache -s - name: Run unit and integration tests for coverage run: | From 09017a5269a1d431aa74b3f4a508979ce5140645 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 27 Feb 2026 08:28:46 +0100 Subject: [PATCH 11/14] perf(ci): split Jazzy into parallel lint and test jobs Removes Jazzy from the build-and-test matrix and creates dedicated jazzy-build, jazzy-lint, and jazzy-test jobs. Lint and test run in parallel after the build job completes, reducing wall-clock time by ~3min (previously sequential: build -> lint -> test). Humble and Rolling remain as single build-and-test matrix jobs. --- .github/workflows/ci.yml | 201 +++++++++++++++++++++++++++++++++++---- 1 file changed, 184 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f53932f4..685a5bc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,6 @@ jobs: fail-fast: false matrix: include: - - ros_distro: jazzy - os_image: ubuntu:noble - ros_distro: humble os_image: ubuntu:jammy - ros_distro: rolling @@ -69,13 +67,9 @@ jobs: run: | apt-get update apt-get install -y ros-${{ matrix.ros_distro }}-test-msgs - # Linter tools only needed on Jazzy (clang versions differ across Ubuntu releases) - if [ "${{ matrix.ros_distro }}" = "jazzy" ]; then - apt-get install -y clang-format clang-tidy - fi source /opt/ros/${{ matrix.ros_distro }}/setup.bash rosdep update - # On Humble, skip the libcpp-httplib-dev rosdep key — the apt version (0.10.3) + # On Humble, skip the libcpp-httplib-dev rosdep key - the apt version (0.10.3) # is too old; cpp-httplib v0.14.3 is built from source in an earlier step. if [ "${{ matrix.ros_distro }}" = "humble" ]; then rosdep install --from-paths src --ignore-src -r -y --skip-keys="libcpp-httplib-dev" @@ -90,28 +84,201 @@ jobs: CCACHE_SLOPPINESS: pch_defines,time_macros run: | source /opt/ros/${{ matrix.ros_distro }}/setup.bash - CMAKE_EXTRA_ARGS="" - if [ "${{ matrix.ros_distro }}" = "jazzy" ]; then - CMAKE_EXTRA_ARGS="-DENABLE_CLANG_TIDY=ON" - fi colcon build --symlink-install \ - --cmake-args -DCMAKE_BUILD_TYPE=Release ${CMAKE_EXTRA_ARGS} \ + --cmake-args -DCMAKE_BUILD_TYPE=Release \ --event-handlers console_direct+ ccache -s - - name: Run linters (clang-format, clang-tidy, etc.) - if: matrix.ros_distro == 'jazzy' + - name: Run unit and integration tests + timeout-minutes: 15 run: | source /opt/ros/${{ matrix.ros_distro }}/setup.bash source install/setup.bash + colcon test --return-code-on-test-failure \ + --ctest-args -LE linter \ + --event-handlers console_direct+ + + - name: Show test results + if: always() + run: colcon test-result --verbose + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.ros_distro }} + path: | + log/ + build/*/test_results/ + + jazzy-build: + runs-on: ubuntu-latest + container: + image: ubuntu:noble + timeout-minutes: 30 + defaults: + run: + shell: bash + + steps: + - name: Install Git + run: | + apt-get update + apt-get install -y git + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up ROS 2 Jazzy + uses: ros-tooling/setup-ros@v0.7 + with: + required-ros-distributions: jazzy + + - name: Install ccache + run: apt-get install -y ccache + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: /root/.cache/ccache + key: ccache-jazzy-${{ github.sha }} + restore-keys: | + ccache-jazzy- + + - name: Install dependencies + run: | + apt-get update + apt-get install -y ros-jazzy-test-msgs clang-format clang-tidy + source /opt/ros/jazzy/setup.bash + rosdep update + rosdep install --from-paths src --ignore-src -y + + - name: Build packages + env: + CCACHE_DIR: /root/.cache/ccache + CCACHE_MAXSIZE: 500M + CCACHE_SLOPPINESS: pch_defines,time_macros + run: | + source /opt/ros/jazzy/setup.bash + colcon build --symlink-install \ + --cmake-args -DCMAKE_BUILD_TYPE=Release -DENABLE_CLANG_TIDY=ON \ + --event-handlers console_direct+ + ccache -s + + - name: Package build artifacts + run: tar cf /tmp/jazzy-build.tar build/ install/ + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: jazzy-build + path: /tmp/jazzy-build.tar + retention-days: 1 + + jazzy-lint: + needs: jazzy-build + runs-on: ubuntu-latest + container: + image: ubuntu:noble + timeout-minutes: 30 + defaults: + run: + shell: bash + + steps: + - name: Install Git + run: | + apt-get update + apt-get install -y git + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up ROS 2 Jazzy + uses: ros-tooling/setup-ros@v0.7 + with: + required-ros-distributions: jazzy + + - name: Install dependencies + run: | + apt-get update + apt-get install -y clang-format clang-tidy + source /opt/ros/jazzy/setup.bash + rosdep update + rosdep install --from-paths src --ignore-src -y + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: jazzy-build + + - name: Extract build artifacts + run: tar xf jazzy-build.tar && rm jazzy-build.tar + + - name: Run linters + run: | + source /opt/ros/jazzy/setup.bash + source install/setup.bash colcon test --return-code-on-test-failure \ --ctest-args -L linter \ --event-handlers console_direct+ + - name: Show test results + if: always() + run: colcon test-result --verbose + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-jazzy-lint + path: | + log/ + build/*/test_results/ + + jazzy-test: + needs: jazzy-build + runs-on: ubuntu-latest + container: + image: ubuntu:noble + timeout-minutes: 15 + defaults: + run: + shell: bash + + steps: + - name: Install Git + run: | + apt-get update + apt-get install -y git + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up ROS 2 Jazzy + uses: ros-tooling/setup-ros@v0.7 + with: + required-ros-distributions: jazzy + + - name: Install dependencies + run: | + apt-get update + apt-get install -y ros-jazzy-test-msgs + source /opt/ros/jazzy/setup.bash + rosdep update + rosdep install --from-paths src --ignore-src -y + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: jazzy-build + + - name: Extract build artifacts + run: tar xf jazzy-build.tar && rm jazzy-build.tar + - name: Run unit and integration tests - timeout-minutes: 15 run: | - source /opt/ros/${{ matrix.ros_distro }}/setup.bash + source /opt/ros/jazzy/setup.bash source install/setup.bash colcon test --return-code-on-test-failure \ --ctest-args -LE linter \ @@ -125,7 +292,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: test-results-${{ matrix.ros_distro }} + name: test-results-jazzy-test path: | log/ build/*/test_results/ From 1e2042f9d78f6f849307a9e62dc3b5a116511a0f Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 27 Feb 2026 11:39:58 +0100 Subject: [PATCH 12/14] docs: update README with test.sh, pre-push hook, and new CI structure - Replace raw colcon test commands with scripts/test.sh presets - Add pre-push hook section documenting clang-tidy-diff.sh and merge-compile-commands.sh setup - Update CI/CD section to reflect the split Jazzy build/lint/test jobs --- README.md | 54 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 9b8acad4..35c2346d 100644 --- a/README.md +++ b/README.md @@ -137,36 +137,43 @@ colcon build --symlink-install ### Testing -Run all tests: +Use the `scripts/test.sh` convenience script: ```bash source install/setup.bash -colcon test -colcon test-result --verbose +./scripts/test.sh # unit tests only (default) +./scripts/test.sh integ # integration tests only +./scripts/test.sh lint # linters (excluding clang-tidy) +./scripts/test.sh tidy # clang-tidy only (slow, ~8-10 min) +./scripts/test.sh all # everything +./scripts/test.sh # single test by CTest name regex ``` -Run linters: +You can pass extra colcon arguments after the preset: ```bash -source install/setup.bash -colcon test --ctest-args -L linters -colcon test-result --verbose +./scripts/test.sh unit --packages-select ros2_medkit_gateway ``` -Run only unit tests (everything except integration): +### Pre-push Hook (clang-tidy) + +An incremental clang-tidy check runs automatically on `git push`, analyzing only changed `.cpp` files. Typical run takes 5-30s vs 8-10 min for a full analysis. + +Setup: ```bash -source install/setup.bash -colcon test --ctest-args -E test_integration -colcon test-result --verbose +# Build the merged compile_commands.json (required once after build) +./scripts/merge-compile-commands.sh + +# Install the pre-push hook +cp scripts/clang-tidy-diff.sh .git/hooks/pre-push +chmod +x .git/hooks/pre-push ``` -Run only integration tests: +To run manually without pushing: ```bash -source install/setup.bash -colcon test --ctest-args -R test_integration -colcon test-result --verbose +./scripts/clang-tidy-diff.sh ``` ### Code Coverage @@ -205,18 +212,23 @@ Then open `coverage_html/index.html` in your browser. ### CI/CD All pull requests and pushes to main are automatically built and tested using GitHub Actions. -The CI workflow runs a build matrix across **ROS 2 Jazzy** (Ubuntu 24.04), **ROS 2 Humble** (Ubuntu 22.04), and **ROS 2 Rolling** (Ubuntu 24.04, allow-failure) and consists of the following jobs: +The CI workflow tests across **ROS 2 Jazzy** (Ubuntu 24.04), **ROS 2 Humble** (Ubuntu 22.04), and **ROS 2 Rolling** (Ubuntu 24.04, allow-failure): -**build-and-test** (matrix: Jazzy + Humble + Rolling): +**build-and-test** (matrix: Humble + Rolling): -- Full build and unit/integration tests on all distros +- Full build with ccache and unit/integration tests - Rolling jobs are allowed to fail (best-effort forward-compatibility) -- Code linting and formatting checks (clang-format, clang-tidy) — Jazzy only + +**jazzy-build** / **jazzy-lint** / **jazzy-test**: + +- `jazzy-build` compiles all packages with ccache and clang-tidy enabled +- `jazzy-lint` and `jazzy-test` run in parallel after the build completes +- Linting includes clang-format, clang-tidy, copyright, cmake-lint, and more **coverage** (Jazzy only): -- Builds with coverage instrumentation (Debug mode) -- Runs unit tests only (for stable coverage metrics) +- Builds with coverage instrumentation (Debug mode, ccache-enabled) +- Runs unit and integration tests (excluding linters) - Generates lcov coverage report (available as artifact) - Uploads coverage to Codecov (only on push to main) From 5a5747b87b6885976d2391ce519bfacb753b8ea2 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 27 Feb 2026 19:19:41 +0100 Subject: [PATCH 13/14] docs: use pre-commit for pre-push hook instead of manual cp Replace manual `cp scripts/clang-tidy-diff.sh .git/hooks/pre-push` instructions with `pre-commit install --hook-type pre-push`, consistent with .pre-commit-config.yaml and .devcontainer/setup-env.sh. --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 35c2346d..df887788 100644 --- a/README.md +++ b/README.md @@ -110,11 +110,13 @@ This section is for contributors and developers who want to build and test ros2_ ### Pre-commit Hooks This project uses [pre-commit](https://pre-commit.com/) to automatically run -`clang-format`, `flake8`, and other checks on staged files before each commit. +`clang-format`, `flake8`, and other checks on staged files before each commit, +plus an incremental clang-tidy check on `git push`. ```bash pip install pre-commit pre-commit install +pre-commit install --hook-type pre-push ``` To run all hooks against every file (useful after first setup): @@ -157,17 +159,16 @@ You can pass extra colcon arguments after the preset: ### Pre-push Hook (clang-tidy) -An incremental clang-tidy check runs automatically on `git push`, analyzing only changed `.cpp` files. Typical run takes 5-30s vs 8-10 min for a full analysis. +An incremental clang-tidy check runs automatically on `git push` via pre-commit, analyzing only changed `.cpp` files. Typical run takes 5-30s vs 8-10 min for a full analysis. Setup: ```bash +# Install the pre-push hook (if not already done above) +pre-commit install --hook-type pre-push + # Build the merged compile_commands.json (required once after build) ./scripts/merge-compile-commands.sh - -# Install the pre-push hook -cp scripts/clang-tidy-diff.sh .git/hooks/pre-push -chmod +x .git/hooks/pre-push ``` To run manually without pushing: From dfe974276fbe544ba4d5cffcc260c621d8bb87d4 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 27 Feb 2026 19:22:22 +0100 Subject: [PATCH 14/14] fix(cmake): add include_guard(GLOBAL) to Linting and Ccache modules Consistent with ROS2MedkitCompat.cmake to prevent double-inclusion. --- cmake/ROS2MedkitCcache.cmake | 2 ++ cmake/ROS2MedkitLinting.cmake | 2 ++ 2 files changed, 4 insertions(+) diff --git a/cmake/ROS2MedkitCcache.cmake b/cmake/ROS2MedkitCcache.cmake index 589f05af..4a2f5bce 100644 --- a/cmake/ROS2MedkitCcache.cmake +++ b/cmake/ROS2MedkitCcache.cmake @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +include_guard(GLOBAL) + # Auto-detect and enable ccache if available. # Include this module early in CMakeLists.txt (before add_library/add_executable). # diff --git a/cmake/ROS2MedkitLinting.cmake b/cmake/ROS2MedkitLinting.cmake index 4db1474c..c10f33b7 100644 --- a/cmake/ROS2MedkitLinting.cmake +++ b/cmake/ROS2MedkitLinting.cmake @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +include_guard(GLOBAL) + # Shared linting configuration for ros2_medkit packages. # Include this in CMakeLists.txt BEFORE the if(BUILD_TESTING) block # (alongside include(ROS2MedkitCcache) - CMAKE_MODULE_PATH is already set).