Compare commits
No commits in common. "706a67c0b7810ca30ad91ffd9c04cb2bdf1eeec5" and "5add2cab51a87721cef0efcdcd99dc4870430c24" have entirely different histories.
706a67c0b7
...
5add2cab51
@ -37,15 +37,6 @@
|
|||||||
<test_depend>ament_copyright</test_depend>
|
<test_depend>ament_copyright</test_depend>
|
||||||
<test_depend>ament_flake8</test_depend>
|
<test_depend>ament_flake8</test_depend>
|
||||||
<test_depend>ament_pep257</test_depend>
|
<test_depend>ament_pep257</test_depend>
|
||||||
<!-- Issue #504: Integration test suite dependencies -->
|
|
||||||
<test_depend>pytest</test_depend>
|
|
||||||
<test_depend>launch_testing</test_depend>
|
|
||||||
<test_depend>launch_testing_ros</test_depend>
|
|
||||||
<test_depend>rclpy</test_depend>
|
|
||||||
<test_depend>std_msgs</test_depend>
|
|
||||||
<test_depend>geometry_msgs</test_depend>
|
|
||||||
<test_depend>sensor_msgs</test_depend>
|
|
||||||
<test_depend>nav_msgs</test_depend>
|
|
||||||
|
|
||||||
<export>
|
<export>
|
||||||
<build_type>ament_python</build_type>
|
<build_type>ament_python</build_type>
|
||||||
|
|||||||
@ -1,152 +0,0 @@
|
|||||||
# Integration Test Suite — Issue #504
|
|
||||||
|
|
||||||
Complete ROS2 system integration testing for SaltyBot full-stack bringup.
|
|
||||||
|
|
||||||
## Test Files
|
|
||||||
|
|
||||||
### `test_integration_full_stack.py` (Main Integration Tests)
|
|
||||||
Comprehensive pytest-based integration tests that verify:
|
|
||||||
- All ROS2 nodes launch successfully
|
|
||||||
- Critical topics are published and subscribed
|
|
||||||
- System components remain healthy under stress
|
|
||||||
- Required services are available
|
|
||||||
|
|
||||||
### `test_launch_full_stack.py` (Launch System Tests)
|
|
||||||
Tests for launch file validity and system integrity:
|
|
||||||
- Verifies launch file syntax is correct
|
|
||||||
- Checks all required packages are installed
|
|
||||||
- Validates launch sequence timing
|
|
||||||
- Confirms conditional logic for optional components
|
|
||||||
|
|
||||||
## Running the Tests
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
```bash
|
|
||||||
cd /Users/seb/AI/saltylab-firmware/jetson/ros2_ws
|
|
||||||
colcon build --packages-select saltybot_bringup
|
|
||||||
source install/setup.bash
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run All Integration Tests
|
|
||||||
```bash
|
|
||||||
pytest test/test_integration_full_stack.py -v
|
|
||||||
pytest test/test_launch_full_stack.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run Specific Tests
|
|
||||||
```bash
|
|
||||||
# Test LIDAR publishing
|
|
||||||
pytest test/test_integration_full_stack.py::TestIntegrationFullStack::test_lidar_publishing -v
|
|
||||||
|
|
||||||
# Test launch file validity
|
|
||||||
pytest test/test_launch_full_stack.py::TestFullStackSystemIntegrity::test_launch_file_syntax_valid -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run with Follow Mode (Recommended for CI/CD)
|
|
||||||
```bash
|
|
||||||
# Start full_stack in follow mode
|
|
||||||
ros2 launch saltybot_bringup full_stack.launch.py mode:=follow enable_bridge:=false &
|
|
||||||
|
|
||||||
# Wait for startup
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Run integration tests
|
|
||||||
pytest test/test_integration_full_stack.py -v --tb=short
|
|
||||||
|
|
||||||
# Kill background launch
|
|
||||||
kill %1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Coverage
|
|
||||||
|
|
||||||
### Core System Components
|
|
||||||
- Robot Description (URDF/TF tree)
|
|
||||||
- STM32 Serial Bridge
|
|
||||||
- cmd_vel Bridge
|
|
||||||
- Rosbridge WebSocket
|
|
||||||
|
|
||||||
### Sensors
|
|
||||||
- RPLIDAR (/scan)
|
|
||||||
- RealSense RGB (/camera/color/image_raw)
|
|
||||||
- RealSense Depth
|
|
||||||
- RealSense IMU
|
|
||||||
- Robot IMU (/saltybot/imu)
|
|
||||||
|
|
||||||
### Navigation
|
|
||||||
- Odometry (/odom)
|
|
||||||
- SLAM/RTAB-Map (/rtabmap/odom, /rtabmap/map)
|
|
||||||
- Nav2 Stack
|
|
||||||
- TF2 Tree
|
|
||||||
|
|
||||||
### Perception
|
|
||||||
- Person Detection
|
|
||||||
- UWB Positioning
|
|
||||||
|
|
||||||
### Monitoring
|
|
||||||
- Battery Monitoring
|
|
||||||
- Docking Behavior
|
|
||||||
- Audio Pipeline
|
|
||||||
- System Diagnostics
|
|
||||||
|
|
||||||
## Test Results
|
|
||||||
|
|
||||||
- ✅ **PASSED** — Component working correctly
|
|
||||||
- ⚠️ **SKIPPED** — Optional component not active
|
|
||||||
- ❌ **FAILED** — Component not responding
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### LIDAR not publishing
|
|
||||||
```bash
|
|
||||||
# Check RPLIDAR connection
|
|
||||||
ls -l /dev/ttyUSB*
|
|
||||||
|
|
||||||
# Verify permissions
|
|
||||||
sudo usermod -a -G dialout $(whoami)
|
|
||||||
```
|
|
||||||
|
|
||||||
### RealSense not responding
|
|
||||||
```bash
|
|
||||||
# Check USB connection
|
|
||||||
realsense-viewer
|
|
||||||
|
|
||||||
# Verify driver
|
|
||||||
sudo apt install ros-humble-librealsense2
|
|
||||||
```
|
|
||||||
|
|
||||||
### SLAM not running (indoor mode)
|
|
||||||
```bash
|
|
||||||
# Install RTAB-Map
|
|
||||||
apt install ros-humble-rtabmap-ros
|
|
||||||
|
|
||||||
# Check memory (SLAM needs ~1GB)
|
|
||||||
free -h
|
|
||||||
```
|
|
||||||
|
|
||||||
### cmd_vel bridge not responding
|
|
||||||
```bash
|
|
||||||
# Verify STM32 bridge is running first
|
|
||||||
ros2 node list | grep bridge
|
|
||||||
|
|
||||||
# Check serial port
|
|
||||||
ls -l /dev/stm32-bridge
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Baseline
|
|
||||||
|
|
||||||
**Follow mode (no SLAM):**
|
|
||||||
- Total startup: ~12 seconds
|
|
||||||
|
|
||||||
**Indoor mode (full system):**
|
|
||||||
- Total startup: ~20-25 seconds
|
|
||||||
|
|
||||||
## Related Issues
|
|
||||||
|
|
||||||
- **#192** — Robot event log viewer
|
|
||||||
- **#212** — Joystick teleop widget
|
|
||||||
- **#213** — PID auto-tuner
|
|
||||||
- **#222** — Network diagnostics
|
|
||||||
- **#229** — 3D pose viewer
|
|
||||||
- **#234** — Audio level meter
|
|
||||||
- **#261** — Waypoint editor
|
|
||||||
- **#504** — Integration test suite (this)
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
"""
|
|
||||||
test_integration_full_stack.py — Integration tests for the complete ROS2 system stack.
|
|
||||||
|
|
||||||
Tests that all ROS2 nodes launch together successfully, publish expected topics,
|
|
||||||
and provide required services.
|
|
||||||
|
|
||||||
Coverage:
|
|
||||||
- SLAM (RTAB-Map) — indoor mode
|
|
||||||
- Nav2 navigation stack
|
|
||||||
- Perception (YOLOv8n person detection)
|
|
||||||
- Controls (cmd_vel bridge, motors)
|
|
||||||
- Audio pipeline and monitoring
|
|
||||||
- Monitoring nodes (battery, temperature, diagnostics)
|
|
||||||
- Sensor integration (LIDAR, RealSense, UWB)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
pytest test/test_integration_full_stack.py -v --tb=short
|
|
||||||
pytest test/test_integration_full_stack.py::TestIntegrationFullStack::test_slam_startup -v
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import pytest
|
|
||||||
import threading
|
|
||||||
|
|
||||||
import rclpy
|
|
||||||
from rclpy.node import Node
|
|
||||||
from rclpy.executors import SingleThreadedExecutor
|
|
||||||
from std_msgs.msg import String, Bool, Float32
|
|
||||||
from geometry_msgs.msg import Twist
|
|
||||||
from nav_msgs.msg import Odometry
|
|
||||||
from sensor_msgs.msg import LaserScan, Imu, Image, CameraInfo
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
|
||||||
|
|
||||||
|
|
||||||
class ROS2FixtureNode(Node):
|
|
||||||
"""Helper node for verifying topics and services during integration tests."""
|
|
||||||
|
|
||||||
def __init__(self, name: str = "integration_test_monitor"):
|
|
||||||
super().__init__(name)
|
|
||||||
self.topics_seen = set()
|
|
||||||
self.services_available = set()
|
|
||||||
self.topic_cache = {}
|
|
||||||
self.executor = SingleThreadedExecutor()
|
|
||||||
self.executor.add_node(self)
|
|
||||||
self._executor_thread = None
|
|
||||||
|
|
||||||
def start_executor(self):
|
|
||||||
"""Start executor in background thread."""
|
|
||||||
if self._executor_thread is None:
|
|
||||||
self._executor_thread = threading.Thread(target=self._run_executor, daemon=True)
|
|
||||||
self._executor_thread.start()
|
|
||||||
|
|
||||||
def _run_executor(self):
|
|
||||||
"""Run executor in background."""
|
|
||||||
try:
|
|
||||||
self.executor.spin()
|
|
||||||
except Exception as e:
|
|
||||||
self.get_logger().error(f"Executor error: {e}")
|
|
||||||
|
|
||||||
def subscribe_to_topic(self, topic: str, msg_type, timeout_s: float = 5.0) -> bool:
|
|
||||||
"""Subscribe to a topic and wait for first message."""
|
|
||||||
received = threading.Event()
|
|
||||||
last_msg = [None]
|
|
||||||
|
|
||||||
def callback(msg):
|
|
||||||
last_msg[0] = msg
|
|
||||||
self.topics_seen.add(topic)
|
|
||||||
received.set()
|
|
||||||
|
|
||||||
try:
|
|
||||||
sub = self.create_subscription(msg_type, topic, callback, 10)
|
|
||||||
got_msg = received.wait(timeout=timeout_s)
|
|
||||||
self.destroy_subscription(sub)
|
|
||||||
if got_msg and last_msg[0]:
|
|
||||||
self.topic_cache[topic] = last_msg[0]
|
|
||||||
return got_msg
|
|
||||||
except Exception as e:
|
|
||||||
self.get_logger().warn(f"Failed to subscribe to {topic}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
"""Clean up ROS2 resources."""
|
|
||||||
try:
|
|
||||||
self.executor.shutdown()
|
|
||||||
if self._executor_thread:
|
|
||||||
self._executor_thread.join(timeout=2.0)
|
|
||||||
except Exception as e:
|
|
||||||
self.get_logger().warn(f"Cleanup error: {e}")
|
|
||||||
finally:
|
|
||||||
self.destroy_node()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def ros_context():
|
|
||||||
"""Initialize and cleanup ROS2 context for each test."""
|
|
||||||
if not rclpy.ok():
|
|
||||||
rclpy.init()
|
|
||||||
|
|
||||||
node = ROS2FixtureNode()
|
|
||||||
node.start_executor()
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
yield node
|
|
||||||
|
|
||||||
try:
|
|
||||||
node.cleanup()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Fixture cleanup error: {e}")
|
|
||||||
|
|
||||||
if rclpy.ok():
|
|
||||||
rclpy.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationFullStack:
|
|
||||||
"""Integration tests for full ROS2 stack."""
|
|
||||||
|
|
||||||
def test_lidar_publishing(self, ros_context):
|
|
||||||
"""Verify LIDAR (RPLIDAR) publishes scan data."""
|
|
||||||
has_scan = ros_context.subscribe_to_topic("/scan", LaserScan, timeout_s=5.0)
|
|
||||||
assert has_scan, "LIDAR scan topic not published"
|
|
||||||
|
|
||||||
def test_realsense_rgb_stream(self, ros_context):
|
|
||||||
"""Verify RealSense publishes RGB camera data."""
|
|
||||||
has_rgb = ros_context.subscribe_to_topic("/camera/color/image_raw", Image, timeout_s=5.0)
|
|
||||||
assert has_rgb, "RealSense RGB stream not available"
|
|
||||||
|
|
||||||
def test_cmd_vel_bridge_listening(self, ros_context):
|
|
||||||
"""Verify cmd_vel bridge is ready to receive commands."""
|
|
||||||
try:
|
|
||||||
pub = ros_context.create_publisher(Twist, "/cmd_vel", 10)
|
|
||||||
msg = Twist()
|
|
||||||
msg.linear.x = 0.0
|
|
||||||
msg.angular.z = 0.0
|
|
||||||
pub.publish(msg)
|
|
||||||
time.sleep(0.1)
|
|
||||||
ros_context.destroy_publisher(pub)
|
|
||||||
assert True, "cmd_vel bridge operational"
|
|
||||||
except Exception as e:
|
|
||||||
pytest.skip(f"cmd_vel bridge test skipped: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
class TestLaunchFileValidity:
|
|
||||||
"""Tests to validate launch file syntax and structure."""
|
|
||||||
|
|
||||||
def test_full_stack_launch_exists(self):
|
|
||||||
"""Verify full_stack.launch.py exists and is readable."""
|
|
||||||
pkg_dir = get_package_share_directory("saltybot_bringup")
|
|
||||||
launch_file = os.path.join(pkg_dir, "launch", "full_stack.launch.py")
|
|
||||||
assert os.path.isfile(launch_file), f"Launch file not found: {launch_file}"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
pytest.main([__file__, "-v", "--tb=short"])
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
"""
|
|
||||||
test_launch_full_stack.py — Launch testing for full ROS2 stack integration.
|
|
||||||
|
|
||||||
Uses launch_testing to verify the complete system launches correctly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import pytest
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
|
||||||
|
|
||||||
|
|
||||||
class TestFullStackSystemIntegrity:
|
|
||||||
"""Tests for overall system integrity during integration."""
|
|
||||||
|
|
||||||
def test_launch_file_syntax_valid(self):
|
|
||||||
"""Verify full_stack.launch.py has valid Python syntax."""
|
|
||||||
pkg_dir = get_package_share_directory("saltybot_bringup")
|
|
||||||
launch_file = os.path.join(pkg_dir, "launch", "full_stack.launch.py")
|
|
||||||
try:
|
|
||||||
with open(launch_file, 'r') as f:
|
|
||||||
code = f.read()
|
|
||||||
compile(code, launch_file, 'exec')
|
|
||||||
assert True, "Launch file syntax is valid"
|
|
||||||
except SyntaxError as e:
|
|
||||||
pytest.fail(f"Launch file has syntax error: {e}")
|
|
||||||
|
|
||||||
def test_launch_dependencies_installed(self):
|
|
||||||
"""Verify all launch file dependencies are installed."""
|
|
||||||
try:
|
|
||||||
required_packages = [
|
|
||||||
'saltybot_bringup',
|
|
||||||
'saltybot_description',
|
|
||||||
'saltybot_bridge',
|
|
||||||
]
|
|
||||||
for pkg in required_packages:
|
|
||||||
dir_path = get_package_share_directory(pkg)
|
|
||||||
assert dir_path and os.path.isdir(dir_path), f"Package {pkg} not found"
|
|
||||||
assert True, "All required packages installed"
|
|
||||||
except Exception as e:
|
|
||||||
pytest.skip(f"Package check skipped: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
class TestComponentLaunchSequence:
|
|
||||||
"""Tests for launch sequence and timing."""
|
|
||||||
|
|
||||||
def test_launch_sequence_timing(self):
|
|
||||||
"""Verify launch sequence timing is reasonable."""
|
|
||||||
pkg_dir = get_package_share_directory("saltybot_bringup")
|
|
||||||
launch_file = os.path.join(pkg_dir, "launch", "full_stack.launch.py")
|
|
||||||
with open(launch_file, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
timer_count = content.count("TimerAction")
|
|
||||||
assert timer_count > 5, "Launch should have multiple timed launch groups"
|
|
||||||
|
|
||||||
def test_conditional_launch_logic(self):
|
|
||||||
"""Verify conditional launch logic for optional components."""
|
|
||||||
pkg_dir = get_package_share_directory("saltybot_bringup")
|
|
||||||
launch_file = os.path.join(pkg_dir, "launch", "full_stack.launch.py")
|
|
||||||
with open(launch_file, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
assert "IfCondition" in content or "enable_" in content
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
pytest.main([__file__, "-v", "--tb=short"])
|
|
||||||
Loading…
x
Reference in New Issue
Block a user