moveit2
The MoveIt Motion Planning Framework for ROS 2.
Loading...
Searching...
No Matches
test_logger.cpp
Go to the documentation of this file.
1/*********************************************************************
2 * Software License Agreement (BSD License)
3 *
4 * Copyright (c) 2026, PickNik Inc.
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 *
11 * * Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 * * Redistributions in binary form must reproduce the above
14 * copyright notice, this list of conditions and the following
15 * disclaimer in the documentation and/or other materials provided
16 * with the distribution.
17 * * Neither the name of PickNik Inc. nor the names of its
18 * contributors may be used to endorse or promote products derived
19 * from this software without specific prior written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 * POSSIBILITY OF SUCH DAMAGE.
33 *********************************************************************/
34
35// Regression test for moveit/moveit2#3827: node-backed loggers must be
36// destroyed before their rclcpp::Context calls rcl_shutdown(), not left to
37// run their destructor at static-destruction time -- and must not do so via
38// a reference cycle that would leak instead if rclcpp::shutdown() is never
39// called.
40//
41// registerNodeResetOnPreShutdown() lives in the internal (non-installed)
42// logger_detail.hpp, included by both logger.cpp (compiled into the real
43// moveit_utils library) and this test, so the white-box tests below
44// exercise the exact same code the library uses. This test links against
45// the real moveit_utils rather than recompiling logger.cpp, and does not
46// expose anything through the public moveit/utils/logger.hpp API.
47//
48// getGlobalRootLogger()'s before-rclcpp::init() fallback is covered
49// separately, by its own single-test executable/process
50// (test_logger_before_init.cpp) -- not here, since that behavior can only
51// be observed by the first thing in a process to touch it.
53
55#include <gtest/gtest.h>
56#include <rclcpp/rclcpp.hpp>
57#include <memory>
58#include <string>
59
60namespace
61{
62
63// Each white-box test below uses its own private rclcpp::Context (rather
64// than the process default one) so tests are fully isolated from one
65// another: rclcpp pre/on-shutdown callbacks are never removed from a
66// Context once registered and persist across repeated init() calls on that
67// same context, so reusing the global default context across tests would
68// let one test's callback fire again during a later test's shutdown.
69rclcpp::NodeOptions makeOptionsWithFreshContext(std::shared_ptr<rclcpp::Context>& context_out)
70{
71 context_out = std::make_shared<rclcpp::Context>();
72 context_out->init(0, nullptr);
73 rclcpp::NodeOptions options;
74 options.context(context_out);
75 return options;
76}
77
78TEST(RegisterNodeResetOnPreShutdownTest, ExplicitShutdownDestroysNode)
79{
80 std::shared_ptr<rclcpp::Context> context;
81 rclcpp::NodeOptions options = makeOptionsWithFreshContext(context);
82
83 rclcpp::Node::SharedPtr node = std::make_shared<rclcpp::Node>("logger_reset_test", options);
84 std::weak_ptr<rclcpp::Node> weak_node = node;
85 // Must stay in scope (not just be constructed) until after context->shutdown()
86 // below: it is what the pre-shutdown callback's weak_ptr needs to lock
87 // successfully in order to reset `node`.
88 std::shared_ptr<std::mutex> mutex = moveit::detail::registerNodeResetOnPreShutdown(node);
89
90 EXPECT_FALSE(weak_node.expired());
91
92 // rcl_shutdown() runs as part of this call. If the node were destroyed
93 // only afterwards (e.g. at static destruction), an RMW implementation
94 // whose own process-wide state is torn down around the same time (e.g.
95 // rmw_zenoh_cpp) could abort. The pre-shutdown callback must destroy the
96 // node first.
97 context->shutdown("test shutdown");
98
99 EXPECT_EQ(node, nullptr) << "the caller's own node slot must be reset by the callback";
100 EXPECT_TRUE(weak_node.expired()) << "node must be destroyed before rcl_shutdown(), not after";
101}
102
103// Regression test for a race CodeRabbit flagged in getGlobalRootLogger():
104// once registerNodeResetOnPreShutdown() returns, a concurrent
105// rclcpp::shutdown() can reset the node at any point afterwards, including
106// before the caller's first read of it. This deterministically exercises
107// the worst case of that race -- the reset having already happened by the
108// time the read takes its lock -- without needing actual concurrent
109// threads (which would make the test flaky). It proves the lock-then-check
110// pattern getGlobalRootLogger() and setNodeLoggerName() both use is safe:
111// once the same mutex the callback locks is held, the node is never
112// dereferenced without first being checked for null.
113TEST(RegisterNodeResetOnPreShutdownTest, LockedReadAfterResetDoesNotDereferenceNull)
114{
115 std::shared_ptr<rclcpp::Context> context;
116 rclcpp::NodeOptions options = makeOptionsWithFreshContext(context);
117
118 rclcpp::Node::SharedPtr node = std::make_shared<rclcpp::Node>("race_test_node", options);
119 std::shared_ptr<std::mutex> mutex = moveit::detail::registerNodeResetOnPreShutdown(node);
120
121 // Simulate a shutdown racing ahead of the first locked read.
122 context->shutdown("simulate a shutdown racing ahead of the first read");
123 ASSERT_EQ(node, nullptr);
124
125 std::lock_guard<std::mutex> lock(*mutex);
126 EXPECT_FALSE(static_cast<bool>(node)) << "node must be safely observed as reset while holding the lock";
127}
128
129// Proves the callback does not strongly capture either the node or the
130// mutex, by exercising the "rclcpp::shutdown() is never called" path and
131// checking actual object destruction via weak_ptr expiry (not merely a
132// process exit code):
133//
134// - if the callback strongly captured the mutex, weak_mutex would not
135// expire while the context (which owns the callback) remained alive;
136// - if the callback strongly captured the Node, weak_node would not expire
137// either, since the context owns the callback and the Node owns the
138// context;
139// - with the intended weak/non-owning captures, both expire once the
140// caller's own `node` and `mutex` variables go out of scope, even though
141// the (still-alive) context's registered callback references them.
142TEST(RegisterNodeResetOnPreShutdownTest, DoesNotRetainNodeOrGuardWithoutExplicitShutdown)
143{
144 std::shared_ptr<rclcpp::Context> context;
145 rclcpp::NodeOptions options = makeOptionsWithFreshContext(context);
146
147 std::weak_ptr<rclcpp::Node> weak_node;
148 std::weak_ptr<std::mutex> weak_mutex;
149 {
150 rclcpp::Node::SharedPtr node = std::make_shared<rclcpp::Node>("cycle_test_node", options);
151 weak_node = node;
152 std::shared_ptr<std::mutex> mutex = moveit::detail::registerNodeResetOnPreShutdown(node);
153 weak_mutex = mutex;
154 EXPECT_FALSE(weak_node.expired());
155 EXPECT_FALSE(weak_mutex.expired());
156 // Both `node` and `mutex` (the only strong owners of the node and the
157 // mutex, respectively) go out of scope here, *without* ever calling
158 // context->shutdown() -- this is the "no explicit shutdown" case.
159 }
160
161 EXPECT_TRUE(weak_mutex.expired()) << "mutex must not be kept alive by the callback's capture of it";
162 EXPECT_TRUE(weak_node.expired()) << "node must not be kept alive by the callback's capture of it";
163
164 // The context itself, and its now-dangling (weak-only, already-expired)
165 // callback registration, can be safely torn down too.
166 context->shutdown("test cleanup");
167}
168
169// Black-box test of the actual public API, using the process-wide default
170// rclcpp context (the same one moveit::setNodeLoggerName() and
171// moveit::getGlobalRootLogger() use internally via plain rclcpp::init()),
172// rather than a private test context. This exercises the same
173// init/use/shutdown sequence as the issue #3827 reporter's MWE, plus a
174// second call afterwards to guard against a post-shutdown null dereference.
175TEST(SetNodeLoggerNameTest, SafeAcrossExplicitShutdown)
176{
177 rclcpp::init(0, nullptr);
178
179 moveit::setNodeLoggerName("logger_black_box_test");
180 const rclcpp::Logger logger_before_shutdown = moveit::getLogger("child");
181 ASSERT_NE(logger_before_shutdown.get_name(), nullptr);
182 const std::string name_before_shutdown = logger_before_shutdown.get_name();
183 try
184 {
185 RCLCPP_INFO(logger_before_shutdown, "before shutdown");
186 }
187 catch (const std::exception& ex)
188 {
189 FAIL() << "logging before shutdown must not throw: " << ex.what();
190 }
191
192 rclcpp::shutdown();
193
194 // The pre-shutdown callback has now reset setNodeLoggerName()'s node, but
195 // the rclcpp::Logger previously assigned into getGlobalRootLogger() is
196 // unaffected: rclcpp::Logger owns its logger-name state independently and
197 // does not retain a reference to the Node, so its name -- and every
198 // logger derived from it via get_child() -- is still valid and unchanged
199 // here.
200 const rclcpp::Logger logger_after_shutdown = moveit::getLogger("child");
201 ASSERT_NE(logger_after_shutdown.get_name(), nullptr);
202 EXPECT_STREQ(logger_after_shutdown.get_name(), name_before_shutdown.c_str());
203
204 // A second call after shutdown, and continuing to log through the (now
205 // node-less) logger, must not crash: this is a regression guard for the
206 // "first call wins" static being reset out from under a later caller. The
207 // name is deliberately unchanged from before shutdown: with the node
208 // already reset, setNodeLoggerName() leaves getGlobalRootLogger() as-is
209 // rather than dereferencing the destroyed node.
210 try
211 {
212 moveit::setNodeLoggerName("logger_black_box_test_after_shutdown");
213 const rclcpp::Logger logger_after_second_call = moveit::getLogger("child");
214 ASSERT_NE(logger_after_second_call.get_name(), nullptr);
215 EXPECT_STREQ(logger_after_second_call.get_name(), name_before_shutdown.c_str())
216 << "setNodeLoggerName() must not change the logger after its node has already been reset";
217 RCLCPP_INFO(logger_after_second_call, "after shutdown");
218 }
219 catch (const std::exception& ex)
220 {
221 FAIL() << "setNodeLoggerName()/logging after shutdown must not throw: " << ex.what();
222 }
223}
224
225} // namespace
226
227int main(int argc, char** argv)
228{
229 testing::InitGoogleTest(&argc, argv);
230 return RUN_ALL_TESTS();
231}
std::shared_ptr< std::mutex > registerNodeResetOnPreShutdown(rclcpp::Node::SharedPtr &node)
rclcpp::Logger getLogger(const std::string &name)
Creates a namespaced logger.
Definition logger.cpp:106
void setNodeLoggerName(const std::string &name)
Call once after creating a node to initialize logging namespaces.
Definition logger.cpp:88
TEST(AllValid, Instantiate)
int main(int argc, char **argv)