Skip to content

Commit d948cd6

Browse files
committed
Merge branch 'main' into UpdateCamSync
2 parents 54dec78 + 09d26e8 commit d948cd6

File tree

5 files changed

+105
-18
lines changed

5 files changed

+105
-18
lines changed

.github/actions/build-and-test/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ runs:
3434
- name: ✅ Run tests
3535
shell: bash
3636
run: |
37+
source install/setup.bash
3738
source /opt/ros/${{ inputs.ros-distro }}/setup.bash
3839
IS_CI=1 colcon test --merge-install --event-handlers console_cohesion+
3940
colcon test-result --verbose

deep_sample/test/launch_tests/test_sample_gpu_backend.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@
3636
import numpy as np
3737

3838

39+
def _is_gpu_available():
40+
"""Check if GPU and CUDA libraries are available."""
41+
try:
42+
import ctypes
43+
44+
# Try to load CUDA runtime library
45+
ctypes.CDLL("libcuda.so.1")
46+
return True
47+
except (OSError, AttributeError):
48+
return False
49+
50+
3951
@pytest.mark.launch_test
4052
def generate_test_description():
4153
"""Generate launch description for GPU backend test."""
@@ -77,6 +89,9 @@ def generate_test_description():
7789
)
7890

7991

92+
@unittest.skipUnless(
93+
_is_gpu_available(), "GPU/CUDA not available - skipping GPU backend tests"
94+
)
8095
class TestGPUBackend(unittest.TestCase):
8196
"""Test GPU backend functionality."""
8297

deep_sample/test/launch_tests/test_sample_tensorrt_backend.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@
3636
import numpy as np
3737

3838

39+
def _is_gpu_available():
40+
"""Check if GPU and CUDA libraries are available."""
41+
try:
42+
import ctypes
43+
44+
# Try to load CUDA runtime library
45+
ctypes.CDLL("libcuda.so.1")
46+
return True
47+
except (OSError, AttributeError):
48+
return False
49+
50+
3951
@pytest.mark.launch_test
4052
def generate_test_description():
4153
"""Generate launch description for TensorRT backend test."""
@@ -77,6 +89,9 @@ def generate_test_description():
7789
)
7890

7991

92+
@unittest.skipUnless(
93+
_is_gpu_available(), "GPU/CUDA not available - skipping TensorRT backend tests"
94+
)
8095
class TestTensorRTBackend(unittest.TestCase):
8196
"""Test TensorRT backend functionality."""
8297

deep_test/include/test_fixtures/test_executor_fixture.hpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#pragma once
1616

17+
#include <atomic>
1718
#include <memory>
1819
#include <thread>
1920
#include <type_traits>
@@ -62,6 +63,12 @@ class TestExecutorFixture : private ROS2Initializer
6263
*/
6364
void start_spinning();
6465

66+
/**
67+
* @brief Stop spinning the executor
68+
* Can be called to cleanly stop the executor before fixture destruction
69+
*/
70+
void stop_spinning();
71+
6572
/**
6673
* @brief Add a templated test node to be spun by this executor
6774
*
@@ -83,9 +90,22 @@ class TestExecutorFixture : private ROS2Initializer
8390
}
8491
}
8592

93+
/**
94+
* @brief Remove a node from the executor
95+
*
96+
* @tparam T The test node type
97+
* @param node The test node to remove
98+
*/
99+
template <typename T>
100+
void remove_node(std::shared_ptr<T> node)
101+
{
102+
executor_.remove_node(node->get_node_base_interface());
103+
}
104+
86105
protected:
87106
rclcpp::executors::SingleThreadedExecutor executor_;
88107
std::thread spin_thread_;
108+
std::atomic<bool> should_spin_{true};
89109
std::vector<std::shared_ptr<rclcpp_lifecycle::LifecycleNode>> lifecycle_nodes_;
90110
};
91111

deep_test/src/test_executor_fixture.cpp

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,38 @@
1414

1515
#include "test_fixtures/test_executor_fixture.hpp"
1616

17+
#include <atomic>
18+
#include <mutex>
19+
1720
#include <lifecycle_msgs/msg/state.hpp>
1821

1922
namespace deep_ros::test
2023
{
2124

25+
// Static members for safe one-time initialization
26+
static std::mutex ros_init_mutex;
27+
static std::atomic<int> ros_init_count{0};
28+
static std::atomic<bool> ros_initialized{false};
29+
2230
ROS2Initializer::ROS2Initializer()
2331
{
24-
rclcpp::init(0, nullptr);
32+
std::lock_guard<std::mutex> lock(ros_init_mutex);
33+
if (!ros_initialized.load()) {
34+
rclcpp::init(0, nullptr);
35+
ros_initialized.store(true);
36+
}
37+
ros_init_count++;
2538
}
2639

2740
ROS2Initializer::~ROS2Initializer()
2841
{
29-
rclcpp::shutdown();
42+
std::lock_guard<std::mutex> lock(ros_init_mutex);
43+
ros_init_count--;
44+
// Only shutdown when the last instance is destroyed
45+
if (ros_init_count.load() == 0 && ros_initialized.load()) {
46+
rclcpp::shutdown();
47+
ros_initialized.store(false);
48+
}
3049
}
3150

3251
TestExecutorFixture::TestExecutorFixture()
@@ -35,31 +54,48 @@ TestExecutorFixture::TestExecutorFixture()
3554

3655
TestExecutorFixture::~TestExecutorFixture()
3756
{
38-
// Deactivate all lifecycle nodes to ensure proper cleanup
57+
stop_spinning();
58+
59+
// Now that spinning has stopped, we can safely deactivate lifecycle nodes
3960
for (auto & node : lifecycle_nodes_) {
40-
if (node && node->get_current_state().id() == lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) {
41-
try {
42-
node->deactivate();
43-
} catch (const std::exception & e) {
44-
// Log but don't throw during destruction
45-
RCLCPP_WARN(
46-
rclcpp::get_logger("TestExecutorFixture"), "Failed to deactivate node during cleanup: %s", e.what());
47-
}
61+
if (!node) {
62+
continue;
4863
}
49-
}
50-
51-
// Cancel executor and wait for thread to finish
52-
executor_.cancel();
5364

54-
if (spin_thread_.joinable()) {
55-
spin_thread_.join();
65+
auto state = node->get_current_state().id();
66+
if (state == lifecycle_msgs::msg::State::PRIMARY_STATE_ACTIVE) {
67+
node->deactivate();
68+
}
5669
}
70+
71+
lifecycle_nodes_.clear();
5772
}
5873

5974
void TestExecutorFixture::start_spinning()
6075
{
6176
if (!spin_thread_.joinable()) {
62-
spin_thread_ = std::thread([this]() { executor_.spin(); });
77+
should_spin_ = true;
78+
spin_thread_ = std::thread([this]() {
79+
// Use spin_once with timeout instead of blocking spin()
80+
// This allows the thread to exit when should_spin_ is set to false
81+
while (should_spin_.load()) {
82+
executor_.spin_once(std::chrono::milliseconds(100));
83+
}
84+
});
85+
}
86+
}
87+
88+
void TestExecutorFixture::stop_spinning()
89+
{
90+
should_spin_ = false;
91+
executor_.cancel();
92+
93+
if (spin_thread_.joinable()) {
94+
// Give it a moment to exit the loop
95+
if (!spin_thread_.joinable()) {
96+
return; // Already joined
97+
}
98+
spin_thread_.join();
6399
}
64100
}
65101

0 commit comments

Comments
 (0)