# Brain 包单元测试说明

本文档介绍如何为 `brain` 包搭建 Google Test 测试环境、构建与运行测试，以及测试覆盖范围与方法说明。所有测试文件均放置在独立目录 `src/brain/test/`，与运行时可执行程序解耦。

## 测试环境

- 操作系统：Linux
- 构建系统：colcon + ament_cmake（ROS 2）
- 主要依赖：
  - ament_cmake_gtest
  - rclcpp、rclcpp_action、rclcpp_lifecycle
  - behaviortree_cpp
  - smacc2、smacc2_msgs
  - 本工程内的 `interfaces`（消息/动作定义）

若 ROS 2 安装不完整，请根据发行版安装缺失组件或通过工作区覆盖安装。

## 环境准备步骤

1. 载入 ROS 2 与工作区环境：

   ```bash
   source /opt/ros/$ROS_DISTRO/setup.bash
   # 在工作区根目录
   source install/setup.bash || true
   ```

2. 确认测试依赖可用：

   `ament_cmake_gtest` 必需。若缺失（Debian/Ubuntu 例）：

   ```bash
   sudo apt-get update
   sudo apt-get install ros-$ROS_DISTRO-ament-cmake-gtest
   ```

3. 启用测试并仅构建 brain 包：

   ```bash
   colcon build --cmake-args -DBUILD_TESTING=ON --packages-select brain
   ```

4. 运行测试：

   ```bash
   colcon test --packages-select brain
   colcon test-result --verbose
   ```

## 可选：本地跳过 Linter（仅为便捷）

若本地仅关注单元测试而不希望被 uncrustify/cmake-lint 等阻塞，可在当前终端临时跳过 ament linter：

```bash
export SKIP_LINT=1
colcon build --cmake-args -DBUILD_TESTING=ON --packages-select brain
colcon test --packages-select brain
colcon test-result --verbose
```

说明：此环境变量仅影响本地构建过程中的 linter 触发，不影响代码逻辑与测试本身。

## 测试目录结构与说明

- `test/test_action_registry.cpp`：覆盖 `ActionServerRegistry` 与 `ActionClientRegistry` 的基础行为。
- `test/test_bt_registry.cpp`：覆盖 `BTRegistry` 的动作注册、序列注册与树构建/执行。
- `test/test_cerebellum_node.cpp`：验证 `CerebellumNode` 启动与示例动作服务端可用性（历史回归）。
- `test/test_cerebrum_node.cpp`：验证 `CerebrumNode` 初始化（参数、BT 注册/构建、定时器）。
- `test/test_sm_cerebellum.cpp`：验证 `sm_cerebellum.hpp` 中 `runtime()` 默认值（占位待扩展）。
- `test/test_execute_bt_action.cpp`：验证统一 ExecuteBtAction 单技能执行（ArmSpaceControl），检查反馈阶段（至少 START / END），最终 result(total_skills=1 succeeded_skills=1) 与 progress≈1.0。

所有测试均与运行时可执行程序解耦，不需要启动 `brain_node`。`test_execute_bt_action.cpp` 内部注入一个最小假 ArmSpaceControl 动作服务器驱动小脑执行逻辑。

## 接口覆盖与测试方法

- `brain/action_registry.hpp`
  - `ActionServerRegistry::register_action_server`：
    - 方法：在测试中注册伪回调（goal/cancel/execute），构造 `rclcpp::Node` 并使用 `SingleThreadedExecutor` 执行 `spin_some()`，验证服务端注册与生命周期管理无异常。
  - `ActionClientRegistry::register_client` / `send`：
    - 方法：通过 `CerebrumNode` 的 BT handler（`MoveJ_H::on_start`）触发 `send_action("move_to_position")`，覆盖客户端注册与发送路径。在未启动真实服务端时仅记录日志，不崩溃。

- `brain/bt_registry.hpp`
  - `register_bt_action` / `register_bt_sequence` / `build_tree_from_sequence`：
    - 方法：注册测试动作与序列，构建树后调用 `tickWhileRunning()`，断言 `on_start` 被触发（标志位 `start_called` 置为 true）。

- `brain/cerebellum_node.hpp`
  - ExecuteBtAction 与技能调度：`test_execute_bt_action.cpp` 发送 ExecuteBtAction goal，断言：success 与反馈阶段集合；验证扩展 Result 字段(total_skills / succeeded_skills)。
  - 回归动作服务可用性：`test_cerebellum_node.cpp` 保留历史最小可用性检查。

- `brain/cerebrum_node.hpp`
  - 节点初始化与行为树构建：
    - 方法：构造节点、读取参数、注册 BT handler/序列并构建，`spin_some()` 验证初始化流程与定时器创建。`MoveJ_H::on_start` 会触发 `send_action("move_to_position")`，覆盖客户端发送路径（不要求服务端存在）。

- `brain/sm_cerebellum.hpp`
  - `runtime()` 默认值：
    - 方法：直接访问静态运行时数据，断言 `current_x/current_y` 初始为 `0.0`。

## 设计与稳定性备注

- 需要使用 ROS executor 的测试均采用 `SingleThreadedExecutor`，并尽量缩短执行时间（仅 `spin_some()` 或短周期）。
- 避免在单元测试中引入完整的端到端系统依赖，通过 handler 与宏开关（`BRAIN_DISABLE_SM`）保证测试可重复、稳定。
- ExecuteBtAction 单技能测试未覆盖 RUN 插值（因为伪服务器快速完成）。如需测试 RUN 进度，可扩展一个延时执行的假服务器并断言收到多个 RUN 阶段反馈(progress 递增 <1.0)。
- 如需更深入的端到端校验，可在测试中同时起一个最小 action server/client 闭环，但建议引入超时与健壮的清理逻辑。

## 故障排查（重要）

- 现象：按本文命令运行后，单测实际已通过，但最终 `colcon test-result` 显示 `uncrustify` 失败（大量“Code style divergence”）。
- 原因：CMake 第一次配置时已把 `ament_lint_auto` 的测试目标写入缓存；仅设置 `SKIP_LINT=1` 不会自动清理之前缓存中登记的 linter 测试目标。
- 解决：在切换 `SKIP_LINT` 或希望临时跳过 Linter 时，需强制重新配置并清理旧测试记录：

```bash
source /opt/ros/$ROS_DISTRO/setup.bash
source install/setup.bash || true
export SKIP_LINT=1
colcon build --cmake-clean-cache --cmake-args -DBUILD_TESTING=ON --packages-select brain
colcon test --packages-select brain --event-handlers console_direct+
colcon test-result --verbose
```

- 说明：以上流程会移除缓存中的 linter 测试目标，仅保留 gtest；若你需要在 CI 中启用 Linter，请移除 `SKIP_LINT` 并修复格式化差异后再运行。

## CI 场景：启用 Linter 并自动修复

- 目的：在 CI 中开启 ament linters 并确保全部通过。
- 步骤：

```bash
source /opt/ros/$ROS_DISTRO/setup.bash
source install/setup.bash || true
unset SKIP_LINT
colcon build --cmake-clean-cache --cmake-args -DBUILD_TESTING=ON --packages-select brain
ament_uncrustify --reformat src/brain
colcon build --packages-select brain
colcon test --packages-select brain --event-handlers console_direct+
colcon test-result --verbose
```

- 说明：首次启用 linters 建议使用 `--cmake-clean-cache` 触发重新配置；若仍看到旧的失败统计，可清理陈旧测试结果后重跑：

```bash
rm -rf build/brain/test_results/brain
colcon test --packages-select brain --event-handlers console_direct+
colcon test-result --verbose
```

## 如何新增 gtest 单元测试（步骤详解）

- 概览：在 `src/brain/test/` 下新增 `test_xxx.cpp`，在 `src/brain/CMakeLists.txt` 添加 `ament_add_gtest(...)` 并声明依赖，然后 `colcon build && colcon test` 即可。

1. 新建测试文件

   在 `src/brain/test/` 下创建文件，例如 `test_my_feature.cpp`：

   ```cpp
   #include <gtest/gtest.h>
   #include <rclcpp/rclcpp.hpp>
   // 如需使用包内头文件
   #include "brain/cerebrum_node.hpp"

   TEST(MyFeature, BasicBehavior) {
     auto node = std::make_shared<brain::CerebrumNode>();
     // 进行最小操作
     rclcpp::executors::SingleThreadedExecutor ex;
     ex.add_node(node);
     ex.spin_some();
     // 基本断言
     SUCCEED();
   }
   ```

   提示：
   - 如需构造 ROS 节点/动作客户端，优先使用 `SingleThreadedExecutor` 与短周期 `spin_some()`，避免长时间阻塞。
   - 若单测包含 `CerebellumNode` 且不希望启动 SMACC2，可参考现有 `test_cerebellum_node.cpp` 在测试目标上加 `BRAIN_DISABLE_SM` 宏（见下文第 2 步）。

2. 在 CMake 中注册测试目标

   编辑 `src/brain/CMakeLists.txt`，在 `if(BUILD_TESTING)` 块中添加：

   ```cmake
   ament_add_gtest(test_my_feature
     test/test_my_feature.cpp
     # 如需直接复用包内源文件，可在此追加，例如：
     # src/cerebrum_node.cpp
   )
   if(TARGET test_my_feature)
     ament_target_dependencies(test_my_feature
       rclcpp
       rclcpp_action
       behaviortree_cpp
       interfaces
       smacc2
       smacc2_msgs
     )
     target_include_directories(test_my_feature PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
     # 如需禁用 SMACC2：
     # target_compile_definitions(test_my_feature PRIVATE BRAIN_DISABLE_SM=1)
     # 如需 Boost 线程：
     # target_link_libraries(test_my_feature Boost::thread)
   endif()
   ```

   要点：
   - 依赖与包含目录参照已有测试目标，按需裁剪。
   - 需要用到包内类实现时，可把对应 `src/*.cpp` 添加进该测试的源文件列表（仅限测试用途，不改变生产可执行文件）。

3. 构建与运行新测试

   仅选择 `brain` 包进行构建与测试：

   ```bash
   source /opt/ros/$ROS_DISTRO/setup.bash
   source install/setup.bash || true
   colcon build --cmake-args -DBUILD_TESTING=ON --packages-select brain
   colcon test --packages-select brain --event-handlers console_direct+
   colcon test-result --verbose
   ```

4. 常见技巧与约束

   - 执行器：优先 `SingleThreadedExecutor` + `spin_some()` 或极短周期计时器，减少时间依赖。
   - 动作客户端/服务端：单元测试阶段可仅验证“注册/可用性/发送路径”，端到端闭环建议放到集成测试。
   - 行为树：构造 `BTRegistry`，注册最小 handler/序列，使用 `tree.tickWhileRunning()` 触发执行（参考 `test_bt_registry.cpp`）。
   - 宏开关：涉及 `CerebellumNode` 时可通过 `BRAIN_DISABLE_SM` 禁用 SMACC2，避免测试时创建服务造成 rcl 上下文异常。
   - Linter：若 CI 启用 linter，请确保通过“CI 场景”小节中的 `ament_uncrustify --reformat` 后再提交。

## 最新一次测试结果快照（2025-09-26 更新：拆分 ExecuteBtAction 扩展用例 & 全部场景稳定通过）

来源命令：

```bash
colcon test --packages-select brain --event-handlers console_direct+
colcon test-result --verbose
```

BuildStamp: `20250926-SPLIT-ACTION-SCENARIOS`

总结：共 15 个 CTest 目标（11 gtest + 4 linter），全部通过。原先集中在单一 `test_execute_bt_action_extended` 文件的 6 个高级场景被拆分为 6 个独立 gtest 可执行文件，避免共享进程内 rclcpp 全局状态导致的 `std::bad_alloc` / 崩溃问题，实现稳定执行与更清晰的失败定位。

| 测试目标 | 类型 | 状态 | 说明（耗时为近似，省略） |
|----------|------|------|---------------------------|
| test_action_registry | gtest | PASS | Action 注册/生命周期 |
| test_bt_registry | gtest | PASS | 行为树序列构建/执行 |
| test_cerebellum_node | gtest | PASS | Cerebellum 启动 + 技能加载 (SM 禁用) |
| test_cerebrum_node | gtest | PASS | Cerebrum 初始化/BT handler 注册 |
| test_sm_cerebellum | gtest | PASS | 运行时默认值 |
| test_execute_bt_action | gtest | PASS | 单技能 ExecuteBtAction 基础成功路径 |
| test_execute_bt_action_extended | gtest | PASS | 仅保留多技能进度分段 (其它场景已拆分) |
| test_execute_bt_action_unsupported | gtest | PASS | 未定义技能立即失败 (Abort) |
| test_execute_bt_action_run_interp | gtest | PASS | 延时服务器产生多个 RUN 反馈 (>=3) |
| test_execute_bt_action_timeout_continue | gtest | PASS | 第一技能超时继续第二技能 (abort_on_failure=false) |
| test_execute_bt_action_stats | gtest | PASS | 统计话题（或为空，定时器禁用时）稳定无崩溃 |
| test_execute_bt_action_cancel | gtest | PASS | 中途 cancel；当前实现返回 SUCCEEDED (兼容未来 CANCELED) |
| cppcheck | linter | PASS | 静态分析 |
| lint_cmake | linter | PASS | CMake 风格 |
| uncrustify | linter | PASS | 代码格式统一 |
| xmllint | linter | PASS | package.xml 合法 |

扩展 ExecuteBtAction 场景拆分后说明：

- test_execute_bt_action_extended (MultiSkillProgressSegmentation)：三技能序列分段进度验证。收集各技能 END 阶段反馈的 progress，断言≈ 1/3, 2/3, 1.0（容差给 0.25 / 0.05）。
- test_execute_bt_action_unsupported：首技能不存在 -> 立即失败，Result code = ABORTED；result.total_skills=2，succeeded_skills=0。
- test_execute_bt_action_run_interp：延时 1.2s 完成的伪服务器触发 CerebellumNode 在 RUN 阶段多次插值反馈（run_count >= 3）。
- test_execute_bt_action_timeout_continue：第一技能挂起触发超时（0.5s），在 abort_on_failure=false 配置下继续第二技能；最终 Result code = ABORTED，total=2 succeeded=1。
- test_execute_bt_action_stats：验证执行期间统计逻辑不崩溃；当 stats 定时器未禁用或日志刷新触发时，可观察到日志/话题输出；允许空发布以适配 timer 关闭情形。
- test_execute_bt_action_cancel：长耗时技能中途调用 cancel；当前内部未传播取消到子动作（仍 SUCCEEDED），断言接受 SUCCEEDED 或未来增强后的 CANCELED（前向兼容）。

拆分动机：单一进程内连续多场景（含延迟、超时、挂起、取消）测试曾出现内存分配异常及进程崩溃。经拆分后，每个场景拥有独立的 rclcpp 初始化与资源生命周期，稳定性显著提升，也使故障定位更直接。

后续可选改进：
 
1. 为 cancel 测试添加显式 on_cancel 处理（CerebellumNode 增加取消传播路径）。
2. stats 测试中设置非零 stats_log_interval_sec（例如 0.2）并等待一次定时器发布，改为强断言包含 seq_ok。当前由于需求宽松保持兼容。
3. 引入模拟时间 (ROS clock) 以缩短等待时间并精确控制 RUN 插值节奏。

