ros2_foxglove_bridge/tests/smoke_test.cpp
Go to the documentation of this file.
1 #include <chrono>
2 #include <filesystem>
3 #include <future>
4 #include <thread>
5 
6 #include <gtest/gtest.h>
7 #include <std_msgs/msg/string.hpp>
8 #include <std_srvs/srv/set_bool.hpp>
9 #include <websocketpp/config/asio_client.hpp>
10 
14 
15 constexpr char URI[] = "ws://localhost:8765";
16 
17 // Binary representation of std_msgs/msg/String for "hello world"
18 constexpr uint8_t HELLO_WORLD_CDR[] = {0, 1, 0, 0, 12, 0, 0, 0, 104, 101,
19  108, 108, 111, 32, 119, 111, 114, 108, 100, 0};
20 constexpr char HELLO_WORLD_JSON[] = "{\"data\": \"hello world\"}";
21 constexpr char STD_MSGS_STRING_SCHEMA[] = "data string";
22 
23 constexpr auto ONE_SECOND = std::chrono::seconds(1);
24 constexpr auto DEFAULT_TIMEOUT = std::chrono::seconds(10);
25 
26 class TestWithExecutor : public testing::Test {
27 protected:
29  this->_executorThread = std::thread([this]() {
30  this->executor.spin();
31  });
32  }
33 
34  ~TestWithExecutor() override {
35  this->executor.cancel();
36  this->_executorThread.join();
37  }
38 
39  rclcpp::executors::SingleThreadedExecutor executor;
40 
41 private:
42  std::thread _executorThread;
43 };
44 
45 class ParameterTest : public TestWithExecutor {
46 public:
47  using PARAM_1_TYPE = std::string;
48  inline static const std::string NODE_1_NAME = "node_1";
49  inline static const std::string PARAM_1_NAME = "string_param";
50  inline static const PARAM_1_TYPE PARAM_1_DEFAULT_VALUE = "hello";
51  inline static const std::string DELETABLE_PARAM_NAME = "deletable_param";
52 
53  using PARAM_2_TYPE = std::vector<int64_t>;
54  inline static const std::string NODE_2_NAME = "node_2";
55  inline static const std::string PARAM_2_NAME = "int_array_param";
56  inline static const PARAM_2_TYPE PARAM_2_DEFAULT_VALUE = {1, 2, 3};
57 
58  using PARAM_3_TYPE = double;
59  inline static const std::string PARAM_3_NAME = "float_param";
60  inline static const PARAM_3_TYPE PARAM_3_DEFAULT_VALUE = 1.123;
61 
62  using PARAM_4_TYPE = std::vector<double>;
63  inline static const std::string PARAM_4_NAME = "float_array_param";
64  inline static const PARAM_4_TYPE PARAM_4_DEFAULT_VALUE = {1.1, 2.2, 3.3};
65 
66 protected:
67  void SetUp() override {
68  auto nodeOptions = rclcpp::NodeOptions();
69  nodeOptions.allow_undeclared_parameters(true);
70  _paramNode1 = rclcpp::Node::make_shared(NODE_1_NAME, nodeOptions);
71  auto p1Param = rcl_interfaces::msg::ParameterDescriptor{};
72  p1Param.name = PARAM_1_NAME;
73  p1Param.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING;
74  p1Param.read_only = false;
75  _paramNode1->declare_parameter(p1Param.name, PARAM_1_DEFAULT_VALUE, p1Param);
76  _paramNode1->set_parameter(rclcpp::Parameter(DELETABLE_PARAM_NAME, true));
77 
78  _paramNode2 = rclcpp::Node::make_shared(NODE_2_NAME);
79  auto p2Param = rcl_interfaces::msg::ParameterDescriptor{};
80  p2Param.name = PARAM_2_NAME;
81  p2Param.type = rcl_interfaces::msg::ParameterType::PARAMETER_INTEGER_ARRAY;
82  p2Param.read_only = false;
83  _paramNode2->declare_parameter(p2Param.name, PARAM_2_DEFAULT_VALUE, p2Param);
84  _paramNode2->declare_parameter(PARAM_3_NAME, PARAM_3_DEFAULT_VALUE);
85  _paramNode2->declare_parameter(PARAM_4_NAME, PARAM_4_DEFAULT_VALUE);
86 
87  executor.add_node(_paramNode1);
88  executor.add_node(_paramNode2);
89 
90  _wsClient = std::make_shared<foxglove::Client<websocketpp::config::asio_client>>();
91  ASSERT_EQ(std::future_status::ready, _wsClient->connect(URI).wait_for(DEFAULT_TIMEOUT));
92  }
93 
94  void TearDown() override {
95  executor.remove_node(_paramNode1);
96  executor.remove_node(_paramNode2);
97  }
98 
99  rclcpp::Node::SharedPtr _paramNode1;
100  rclcpp::Node::SharedPtr _paramNode2;
101  std::shared_ptr<foxglove::Client<websocketpp::config::asio_client>> _wsClient;
102 };
103 
104 class ServiceTest : public TestWithExecutor {
105 public:
106  inline static const std::string SERVICE_NAME = "/foo_service";
107 
108 protected:
109  void SetUp() override {
110  _node = rclcpp::Node::make_shared("node");
111  _service = _node->create_service<std_srvs::srv::SetBool>(
112  SERVICE_NAME, [&](std::shared_ptr<std_srvs::srv::SetBool::Request> req,
113  std::shared_ptr<std_srvs::srv::SetBool::Response> res) {
114  res->message = "hello";
115  res->success = req->data;
116  });
117  executor.add_node(_node);
118  }
119 
120  void TearDown() override {
121  executor.remove_node(_node);
122  }
123 
124  rclcpp::Node::SharedPtr _node;
125  rclcpp::ServiceBase::SharedPtr _service;
126  std::shared_ptr<foxglove::Client<websocketpp::config::asio_client>> _wsClient;
127 };
128 
130  : public TestWithExecutor,
131  public testing::WithParamInterface<std::pair<std::string, std::vector<uint8_t>>> {};
132 
134 public:
135  inline static const std::string TOPIC_NAME = "/some_topic";
136 
137 protected:
138  void SetUp() override {
139  _node = rclcpp::Node::make_shared("node");
140  _publisher =
141  _node->create_publisher<std_msgs::msg::String>(TOPIC_NAME, rclcpp::SystemDefaultsQoS());
142  executor.add_node(_node);
143  }
144 
145  void TearDown() override {
146  executor.remove_node(_node);
147  }
148 
149  rclcpp::Node::SharedPtr _node;
150  rclcpp::PublisherBase::SharedPtr _publisher;
151 };
152 
153 template <class T>
154 std::shared_ptr<rclcpp::SerializedMessage> serializeMsg(const T* msg) {
155  using rosidl_typesupport_cpp::get_message_type_support_handle;
156  auto typeSupportHdl = get_message_type_support_handle<T>();
157  auto result = std::make_shared<rclcpp::SerializedMessage>();
158  rmw_ret_t ret = rmw_serialize(msg, typeSupportHdl, &result->get_rcl_serialized_message());
159  EXPECT_EQ(ret, RMW_RET_OK);
160  return result;
161 }
162 
163 template <class T>
164 std::shared_ptr<T> deserializeMsg(const rcl_serialized_message_t* msg) {
165  using rosidl_typesupport_cpp::get_message_type_support_handle;
166  auto typeSupportHdl = get_message_type_support_handle<T>();
167  auto result = std::make_shared<T>();
168  rmw_ret_t ret = rmw_deserialize(msg, typeSupportHdl, result.get());
169  EXPECT_EQ(ret, RMW_RET_OK);
170  return result;
171 }
172 
173 TEST(SmokeTest, testConnection) {
175  EXPECT_EQ(std::future_status::ready, wsClient.connect(URI).wait_for(DEFAULT_TIMEOUT));
176 }
177 
178 TEST(SmokeTest, testSubscription) {
179  // Publish a string message on a latched ros topic
180  const std::string topic_name = "/pub_topic";
181  std_msgs::msg::String rosMsg;
182  rosMsg.data = "hello world";
183 
184  auto node = rclcpp::Node::make_shared("tester");
185  rclcpp::QoS qos = rclcpp::QoS{rclcpp::KeepLast(1lu)};
186  qos.reliable();
187  qos.transient_local();
188  auto pub = node->create_publisher<std_msgs::msg::String>(topic_name, qos);
189  pub->publish(rosMsg);
190 
191  // Connect a few clients and make sure that they receive the correct message
192  const auto clientCount = 3;
193  for (auto i = 0; i < clientCount; ++i) {
194  // Set up a client and subscribe to the channel.
195  auto client = std::make_shared<foxglove::Client<websocketpp::config::asio_client>>();
196  auto channelFuture = foxglove::waitForChannel(client, topic_name);
197  ASSERT_EQ(std::future_status::ready, client->connect(URI).wait_for(ONE_SECOND));
198  ASSERT_EQ(std::future_status::ready, channelFuture.wait_for(ONE_SECOND));
199  const foxglove::Channel channel = channelFuture.get();
200  const foxglove::SubscriptionId subscriptionId = 1;
201 
202  // Subscribe to the channel and confirm that the promise resolves
203  auto msgFuture = waitForChannelMsg(client.get(), subscriptionId);
204  client->subscribe({{subscriptionId, channel.id}});
205  ASSERT_EQ(std::future_status::ready, msgFuture.wait_for(ONE_SECOND));
206  const auto msgData = msgFuture.get();
207  ASSERT_EQ(sizeof(HELLO_WORLD_CDR), msgData.size());
208  EXPECT_EQ(0, std::memcmp(HELLO_WORLD_CDR, msgData.data(), msgData.size()));
209 
210  // Unsubscribe from the channel again.
211  client->unsubscribe({subscriptionId});
212  }
213 }
214 
215 TEST(SmokeTest, testSubscriptionParallel) {
216  // Publish a string message on a latched ros topic
217  const std::string topic_name = "/pub_topic";
218  std_msgs::msg::String rosMsg;
219  rosMsg.data = "hello world";
220 
221  auto node = rclcpp::Node::make_shared("tester");
222  rclcpp::QoS qos = rclcpp::QoS{rclcpp::KeepLast(1lu)};
223  qos.reliable();
224  qos.transient_local();
225  auto pub = node->create_publisher<std_msgs::msg::String>(topic_name, qos);
226  pub->publish(rosMsg);
227 
228  // Connect a few clients (in parallel) and make sure that they receive the correct message
229  const foxglove::SubscriptionId subscriptionId = 1;
230  auto clients = {
231  std::make_shared<foxglove::Client<websocketpp::config::asio_client>>(),
233  std::make_shared<foxglove::Client<websocketpp::config::asio_client>>(),
234  };
235 
236  std::vector<std::future<std::vector<uint8_t>>> futures;
237  for (auto client : clients) {
238  futures.push_back(waitForChannelMsg(client.get(), subscriptionId));
239  }
240 
241  for (auto client : clients) {
242  auto channelFuture = foxglove::waitForChannel(client, topic_name);
243  ASSERT_EQ(std::future_status::ready, client->connect(URI).wait_for(ONE_SECOND));
244  ASSERT_EQ(std::future_status::ready, channelFuture.wait_for(ONE_SECOND));
245  const foxglove::Channel channel = channelFuture.get();
246  client->subscribe({{subscriptionId, channel.id}});
247  }
248 
249  for (auto& future : futures) {
250  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
251  auto msgData = future.get();
252  ASSERT_EQ(sizeof(HELLO_WORLD_CDR), msgData.size());
253  EXPECT_EQ(0, std::memcmp(HELLO_WORLD_CDR, msgData.data(), msgData.size()));
254  }
255 
256  for (auto client : clients) {
257  client->unsubscribe({subscriptionId});
258  }
259 }
260 
261 TEST_P(PublisherTest, testPublishing) {
262  const auto& [encoding, message] = GetParam();
263 
264  foxglove::ClientAdvertisement advertisement;
265  advertisement.channelId = 1;
266  advertisement.topic = "/foo";
267  advertisement.encoding = encoding;
268  advertisement.schemaName = "std_msgs/msg/String";
269  advertisement.schema =
270  std::vector<uint8_t>(STD_MSGS_STRING_SCHEMA, std::end(STD_MSGS_STRING_SCHEMA));
271 
272  // Set up a ROS node with a subscriber
273  std::promise<std::string> msgPromise;
274  auto msgFuture = msgPromise.get_future();
275  auto node = rclcpp::Node::make_shared("tester");
276  auto sub = node->create_subscription<std_msgs::msg::String>(
277  advertisement.topic, 10, [&msgPromise](std::shared_ptr<const std_msgs::msg::String> msg) {
278  msgPromise.set_value(msg->data);
279  });
280  this->executor.add_node(node);
281 
282  // Set up the client, advertise and publish the binary message
283  auto client = std::make_shared<foxglove::Client<websocketpp::config::asio_client>>();
284  ASSERT_EQ(std::future_status::ready, client->connect(URI).wait_for(ONE_SECOND));
285  client->advertise({advertisement});
286 
287  // Wait until the advertisement got advertised as channel by the server
288  auto channelFuture = foxglove::waitForChannel(client, advertisement.topic);
289  ASSERT_EQ(std::future_status::ready, channelFuture.wait_for(ONE_SECOND));
290 
291  // Publish the message and unadvertise again
292  client->publish(advertisement.channelId, message.data(), message.size());
293  client->unadvertise({advertisement.channelId});
294 
295  // Ensure that we have received the correct message via our ROS subscriber
296  ASSERT_EQ(std::future_status::ready, msgFuture.wait_for(DEFAULT_TIMEOUT));
297  this->executor.remove_node(node);
298  EXPECT_EQ("hello world", msgFuture.get());
299 }
300 
302  TestPublishingCDR, PublisherTest,
303  testing::Values(std::make_pair("cdr", std::vector<uint8_t>(HELLO_WORLD_CDR,
304  std::end(HELLO_WORLD_CDR)))));
305 
307  TestPublishingJSON, PublisherTest,
308  testing::Values(std::make_pair("json", std::vector<uint8_t>(HELLO_WORLD_JSON,
309  std::end(HELLO_WORLD_JSON)))));
310 
311 TEST_P(ExistingPublisherTest, testPublishingWithExistingPublisher) {
312  const auto& [encoding, message] = GetParam();
313 
314  foxglove::ClientAdvertisement advertisement;
315  advertisement.channelId = 1;
316  advertisement.topic = TOPIC_NAME;
317  advertisement.encoding = encoding;
318  advertisement.schemaName = "std_msgs/msg/String";
319  advertisement.schema = {}; // Schema intentionally left empty.
320 
321  // Set up a ROS node with a subscriber
322  std::promise<std::string> msgPromise;
323  auto msgFuture = msgPromise.get_future();
324  auto node = rclcpp::Node::make_shared("tester");
325  auto sub = node->create_subscription<std_msgs::msg::String>(
326  advertisement.topic, 10, [&msgPromise](std::shared_ptr<const std_msgs::msg::String> msg) {
327  msgPromise.set_value(msg->data);
328  });
329  rclcpp::executors::SingleThreadedExecutor executor;
330  executor.add_node(node);
331 
332  // Set up the client, advertise and publish the binary message
333  auto client = std::make_shared<foxglove::Client<websocketpp::config::asio_client>>();
334  ASSERT_EQ(std::future_status::ready, client->connect(URI).wait_for(ONE_SECOND));
335  client->advertise({advertisement});
336 
337  // Wait until the advertisement got advertised as channel by the server
338  auto channelFuture = foxglove::waitForChannel(client, advertisement.topic);
339  ASSERT_EQ(std::future_status::ready, channelFuture.wait_for(ONE_SECOND));
340 
341  // Publish the message and unadvertise again
342  client->publish(advertisement.channelId, message.data(), message.size());
343  client->unadvertise({advertisement.channelId});
344 
345  // Ensure that we have received the correct message via our ROS subscriber
346  const auto ret = executor.spin_until_future_complete(msgFuture, ONE_SECOND);
347  ASSERT_EQ(rclcpp::FutureReturnCode::SUCCESS, ret);
348  EXPECT_EQ("hello world", msgFuture.get());
349 }
350 
352  ExistingPublisherTestCDR, ExistingPublisherTest,
353  testing::Values(std::make_pair("cdr", std::vector<uint8_t>(HELLO_WORLD_CDR,
354  std::end(HELLO_WORLD_CDR)))));
355 
357  ExistingPublisherTestJSON, ExistingPublisherTest,
358  testing::Values(std::make_pair("json", std::vector<uint8_t>(HELLO_WORLD_JSON,
359  std::end(HELLO_WORLD_JSON)))));
360 
361 TEST_F(ParameterTest, testGetAllParams) {
362  const std::string requestId = "req-testGetAllParams";
363  auto future = foxglove::waitForParameters(_wsClient, requestId);
364  _wsClient->getParameters({}, requestId);
365  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
366  std::vector<foxglove::Parameter> params = future.get();
367 
368  EXPECT_GE(params.size(), 2UL);
369 }
370 
371 TEST_F(ParameterTest, testGetNonExistingParameters) {
372  const std::string requestId = "req-testGetNonExistingParameters";
373  auto future = foxglove::waitForParameters(_wsClient, requestId);
374  _wsClient->getParameters(
375  {"/foo_1.non_existing_parameter", "/foo_2.non_existing.nested_parameter"}, requestId);
376  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
377  std::vector<foxglove::Parameter> params = future.get();
378 
379  EXPECT_TRUE(params.empty());
380 }
381 
382 TEST_F(ParameterTest, testGetParameters) {
383  const auto p1 = NODE_1_NAME + "." + PARAM_1_NAME;
384  const auto p2 = NODE_2_NAME + "." + PARAM_2_NAME;
385 
386  const std::string requestId = "req-testGetParameters";
387  auto future = foxglove::waitForParameters(_wsClient, requestId);
388  _wsClient->getParameters({p1, p2}, requestId);
389  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
390  std::vector<foxglove::Parameter> params = future.get();
391 
392  EXPECT_EQ(2UL, params.size());
393  auto p1Iter = std::find_if(params.begin(), params.end(), [&p1](const auto& param) {
394  return param.getName() == p1;
395  });
396  auto p2Iter = std::find_if(params.begin(), params.end(), [&p2](const auto& param) {
397  return param.getName() == p2;
398  });
399  ASSERT_NE(p1Iter, params.end());
400  EXPECT_EQ(PARAM_1_DEFAULT_VALUE, p1Iter->getValue().getValue<PARAM_1_TYPE>());
401  ASSERT_NE(p2Iter, params.end());
402 
403  std::vector<int64_t> int_array_val;
404  const auto array_params = p2Iter->getValue().getValue<std::vector<foxglove::ParameterValue>>();
405  for (const auto& paramValue : array_params) {
406  int_array_val.push_back(paramValue.getValue<int64_t>());
407  }
408  EXPECT_EQ(int_array_val, PARAM_2_DEFAULT_VALUE);
409 }
410 
411 TEST_F(ParameterTest, testSetParameters) {
412  const auto p1 = NODE_1_NAME + "." + PARAM_1_NAME;
413  const auto p2 = NODE_2_NAME + "." + PARAM_2_NAME;
414  const PARAM_1_TYPE newP1value = "world";
415  const std::vector<foxglove::ParameterValue> newP2value = {4, 5, 6};
416 
417  const std::vector<foxglove::Parameter> parameters = {
418  foxglove::Parameter(p1, newP1value),
419  foxglove::Parameter(p2, newP2value),
420  };
421 
422  _wsClient->setParameters(parameters);
423  const std::string requestId = "req-testSetParameters";
424  auto future = foxglove::waitForParameters(_wsClient, requestId);
425  _wsClient->getParameters({p1, p2}, requestId);
426  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
427  std::vector<foxglove::Parameter> params = future.get();
428 
429  EXPECT_EQ(2UL, params.size());
430  auto p1Iter = std::find_if(params.begin(), params.end(), [&p1](const auto& param) {
431  return param.getName() == p1;
432  });
433  auto p2Iter = std::find_if(params.begin(), params.end(), [&p2](const auto& param) {
434  return param.getName() == p2;
435  });
436  ASSERT_NE(p1Iter, params.end());
437  EXPECT_EQ(newP1value, p1Iter->getValue().getValue<PARAM_1_TYPE>());
438  ASSERT_NE(p2Iter, params.end());
439 
440  std::vector<int64_t> int_array_val;
441  const auto array_params = p2Iter->getValue().getValue<std::vector<foxglove::ParameterValue>>();
442  for (const auto& paramValue : array_params) {
443  int_array_val.push_back(paramValue.getValue<int64_t>());
444  }
445  const std::vector<int64_t> expected_value = {4, 5, 6};
446  EXPECT_EQ(int_array_val, expected_value);
447 }
448 
449 TEST_F(ParameterTest, testSetParametersWithReqId) {
450  const auto p1 = NODE_1_NAME + "." + PARAM_1_NAME;
451  const PARAM_1_TYPE newP1value = "world";
452  const std::vector<foxglove::Parameter> parameters = {
453  foxglove::Parameter(p1, newP1value),
454  };
455 
456  const std::string requestId = "req-testSetParameters";
457  auto future = foxglove::waitForParameters(_wsClient, requestId);
458  _wsClient->setParameters(parameters, requestId);
459  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
460  std::vector<foxglove::Parameter> params = future.get();
461 
462  EXPECT_EQ(1UL, params.size());
463 }
464 
465 TEST_F(ParameterTest, testSetFloatParametersWithIntegers) {
466  const auto floatParamName = NODE_2_NAME + "." + PARAM_3_NAME;
467  const auto floatArrayParamName = NODE_2_NAME + "." + PARAM_4_NAME;
468  const int64_t floatParamVal = 10;
469  const std::vector<int64_t> floatArrayParamVal = {3, 2, 1};
470  const std::string requestId = "req-testSetFloatParametersWithIntegers";
471  auto future = foxglove::waitForParameters(_wsClient, requestId);
472  const nlohmann::json::array_t parameters = {
473  {{"name", floatParamName}, {"value", floatParamVal}, {"type", "float64"}},
474  {{"name", floatArrayParamName}, {"value", floatArrayParamVal}, {"type", "float64_array"}},
475  };
476  _wsClient->sendText(
477  nlohmann::json{{"op", "setParameters"}, {"id", requestId}, {"parameters", parameters}}.dump());
478  ASSERT_EQ(std::future_status::ready, future.wait_for(ONE_SECOND));
479  std::vector<foxglove::Parameter> params = future.get();
480 
481  {
482  const auto param =
483  std::find_if(params.begin(), params.end(), [floatParamName](const foxglove::Parameter& p) {
484  return p.getName() == floatParamName;
485  });
486  ASSERT_NE(param, params.end());
487  EXPECT_EQ(param->getType(), foxglove::ParameterType::PARAMETER_DOUBLE);
488  EXPECT_NEAR(param->getValue().getValue<double>(), static_cast<double>(floatParamVal), 1e-9);
489  }
490  {
491  const auto param = std::find_if(params.begin(), params.end(),
492  [floatArrayParamName](const foxglove::Parameter& p) {
493  return p.getName() == floatArrayParamName;
494  });
495  ASSERT_NE(param, params.end());
496  EXPECT_EQ(param->getType(), foxglove::ParameterType::PARAMETER_ARRAY);
497  const auto paramValue = param->getValue().getValue<std::vector<foxglove::ParameterValue>>();
498  ASSERT_EQ(paramValue.size(), floatArrayParamVal.size());
499  for (size_t i = 0; i < paramValue.size(); ++i) {
500  EXPECT_NEAR(paramValue[i].getValue<double>(), static_cast<double>(floatArrayParamVal[i]),
501  1e-9);
502  }
503  }
504 }
505 
506 TEST_F(ParameterTest, testUnsetParameter) {
507  const auto p1 = NODE_1_NAME + "." + DELETABLE_PARAM_NAME;
508  const std::vector<foxglove::Parameter> parameters = {
510  };
511 
512  const std::string requestId = "req-testUnsetParameter";
513  auto future = foxglove::waitForParameters(_wsClient, requestId);
514  _wsClient->setParameters(parameters, requestId);
515  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
516  std::vector<foxglove::Parameter> params = future.get();
517 
518  EXPECT_EQ(0UL, params.size());
519 }
520 
521 TEST_F(ParameterTest, testParameterSubscription) {
522  const auto p1 = NODE_1_NAME + "." + PARAM_1_NAME;
523 
524  _wsClient->subscribeParameterUpdates({p1});
525  auto future = foxglove::waitForParameters(_wsClient);
526  _wsClient->setParameters({foxglove::Parameter(p1, "foo")});
527  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
528  std::vector<foxglove::Parameter> params = future.get();
529 
530  ASSERT_EQ(1UL, params.size());
531  EXPECT_EQ(params.front().getName(), p1);
532 
533  _wsClient->unsubscribeParameterUpdates({p1});
534  _wsClient->setParameters({foxglove::Parameter(p1, "bar")});
535 
536  future = foxglove::waitForParameters(_wsClient);
537  ASSERT_EQ(std::future_status::timeout, future.wait_for(ONE_SECOND));
538 }
539 
540 TEST_F(ParameterTest, testGetParametersParallel) {
541  // Connect a few clients (in parallel) and make sure that they all receive parameters
542  auto clients = {
543  std::make_shared<foxglove::Client<websocketpp::config::asio_client>>(),
545  std::make_shared<foxglove::Client<websocketpp::config::asio_client>>(),
546  };
547 
548  std::vector<std::future<std::vector<foxglove::Parameter>>> futures;
549  for (auto client : clients) {
550  futures.push_back(
551  std::async(std::launch::async, [client]() -> std::vector<foxglove::Parameter> {
552  if (std::future_status::ready == client->connect(URI).wait_for(DEFAULT_TIMEOUT)) {
553  const std::string requestId = "req-123";
554  auto future = foxglove::waitForParameters(client, requestId);
555  client->getParameters({}, requestId);
556  future.wait_for(DEFAULT_TIMEOUT);
557  if (future.valid()) {
558  return future.get();
559  }
560  }
561  return {};
562  }));
563  }
564 
565  for (auto& future : futures) {
566  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
567  std::vector<foxglove::Parameter> parameters;
568  EXPECT_NO_THROW(parameters = future.get());
569  EXPECT_GE(parameters.size(), 2UL);
570  }
571 }
572 
573 TEST_F(ServiceTest, testAdvertiseService) {
574  auto client = std::make_shared<foxglove::Client<websocketpp::config::asio_client>>();
575  auto serviceFuture = foxglove::waitForService(client, SERVICE_NAME);
576  ASSERT_EQ(std::future_status::ready, client->connect(URI).wait_for(ONE_SECOND));
577  ASSERT_EQ(std::future_status::ready, serviceFuture.wait_for(DEFAULT_TIMEOUT));
578  const foxglove::Service service = serviceFuture.get();
579 
580  EXPECT_EQ(service.name, SERVICE_NAME);
581  EXPECT_EQ(service.type, "std_srvs/srv/SetBool");
582  EXPECT_EQ(service.requestSchema, "bool data # e.g. for hardware enabling / disabling");
583  EXPECT_EQ(service.responseSchema,
584  "bool success # indicate successful run of triggered service\nstring message # "
585  "informational, e.g. for error messages");
586 }
587 
588 TEST_F(ServiceTest, testCallServiceParallel) {
589  // Connect a few clients (in parallel) and make sure that they can all call the service
590  auto clients = {
591  std::make_shared<foxglove::Client<websocketpp::config::asio_client>>(),
593  std::make_shared<foxglove::Client<websocketpp::config::asio_client>>(),
594  };
595 
596  auto serviceFuture = foxglove::waitForService(*clients.begin(), SERVICE_NAME);
597  for (auto client : clients) {
598  ASSERT_EQ(std::future_status::ready, client->connect(URI).wait_for(ONE_SECOND));
599  }
600  ASSERT_EQ(std::future_status::ready, serviceFuture.wait_for(DEFAULT_TIMEOUT));
601  const foxglove::Service service = serviceFuture.get();
602 
603  std_srvs::srv::SetBool::Request requestMsg;
604  requestMsg.data = true;
605  const auto serializedRequest = serializeMsg(&requestMsg);
606  const auto& serRequestMsg = serializedRequest->get_rcl_serialized_message();
607 
608  foxglove::ServiceRequest request;
609  request.serviceId = service.id;
610  request.callId = 123lu;
611  request.encoding = "cdr";
612  request.data.resize(serRequestMsg.buffer_length);
613  std::memcpy(request.data.data(), serRequestMsg.buffer, serRequestMsg.buffer_length);
614 
615  std::vector<std::future<foxglove::ServiceResponse>> futures;
616  for (auto client : clients) {
617  futures.push_back(foxglove::waitForServiceResponse(client));
618  client->sendServiceRequest(request);
619  }
620 
621  for (auto& future : futures) {
622  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
624  EXPECT_NO_THROW(response = future.get());
625  EXPECT_EQ(response.serviceId, request.serviceId);
626  EXPECT_EQ(response.callId, request.callId);
627  EXPECT_EQ(response.encoding, request.encoding);
628 
629  rclcpp::SerializedMessage serializedResponseMsg(response.data.size());
630  auto& serMsg = serializedResponseMsg.get_rcl_serialized_message();
631  std::memcpy(serMsg.buffer, response.data.data(), response.data.size());
632  serMsg.buffer_length = response.data.size();
633  const auto resMsg = deserializeMsg<std_srvs::srv::SetBool::Response>(&serMsg);
634 
635  EXPECT_EQ(resMsg->message, "hello");
636  EXPECT_EQ(resMsg->success, requestMsg.data);
637  }
638 }
639 
640 TEST_F(ServiceTest, testCallNonexistentService) {
641  auto client = std::make_shared<foxglove::Client<websocketpp::config::asio_client>>();
642  ASSERT_EQ(std::future_status::ready, client->connect(URI).wait_for(std::chrono::seconds(5)));
643 
644  std::promise<nlohmann::json> promise;
645  auto serviceFailureFuture = promise.get_future();
646  client->setTextMessageHandler([&promise](const std::string& payload) mutable {
647  const auto msg = nlohmann::json::parse(payload);
648  if (msg["op"].get<std::string>() == "serviceCallFailure") {
649  promise.set_value(msg);
650  }
651  });
652 
653  foxglove::ServiceRequest request;
654  request.serviceId = 99u;
655  request.callId = 123u;
656  request.encoding = "";
657  request.data = {};
658  client->sendServiceRequest(request);
659 
660  ASSERT_EQ(std::future_status::ready, serviceFailureFuture.wait_for(std::chrono::seconds(5)));
661  const auto failureMsg = serviceFailureFuture.get();
662  EXPECT_EQ(failureMsg["serviceId"].get<foxglove::ServiceId>(), request.serviceId);
663  EXPECT_EQ(failureMsg["callId"].get<uint32_t>(), request.callId);
664 }
665 
666 TEST(SmokeTest, receiveMessagesOfMultipleTransientLocalPublishers) {
667  const std::string topicName = "/latched";
668  auto node = rclcpp::Node::make_shared("node");
669  rclcpp::QoS qos = rclcpp::QoS(rclcpp::KeepLast(1));
670  qos.transient_local();
671  qos.reliable();
672 
673  rclcpp::executors::SingleThreadedExecutor executor;
674  executor.add_node(node);
675  auto spinnerThread = std::thread([&executor]() {
676  executor.spin();
677  });
678 
679  constexpr size_t nPubs = 15;
680  std::vector<rclcpp::Publisher<std_msgs::msg::String>::SharedPtr> pubs;
681  for (size_t i = 0; i < nPubs; ++i) {
682  auto pub = pubs.emplace_back(node->create_publisher<std_msgs::msg::String>(topicName, qos));
683  std_msgs::msg::String msg;
684  msg.data = "Hello";
685  pub->publish(msg);
686  }
687 
688  // Set up a client and subscribe to the channel.
689  auto client = std::make_shared<foxglove::Client<websocketpp::config::asio_client>>();
690  auto channelFuture = foxglove::waitForChannel(client, topicName);
691  ASSERT_EQ(std::future_status::ready, client->connect(URI).wait_for(ONE_SECOND));
692  ASSERT_EQ(std::future_status::ready, channelFuture.wait_for(ONE_SECOND));
693  const foxglove::Channel channel = channelFuture.get();
694  const foxglove::SubscriptionId subscriptionId = 1;
695 
696  // Set up binary message handler to resolve the promise when all nPub message have been received
697  std::promise<void> promise;
698  std::atomic<size_t> nReceivedMessages = 0;
699  client->setBinaryMessageHandler([&promise, &nReceivedMessages](const uint8_t*, size_t) {
700  if (++nReceivedMessages == nPubs) {
701  promise.set_value();
702  }
703  });
704 
705  // Subscribe to the channel and confirm that the promise resolves
706  client->subscribe({{subscriptionId, channel.id}});
707  EXPECT_EQ(std::future_status::ready, promise.get_future().wait_for(DEFAULT_TIMEOUT));
708  EXPECT_EQ(nReceivedMessages, nPubs);
709  client->unsubscribe({subscriptionId});
710 
711  pubs.clear();
712  executor.remove_node(node);
713  executor.cancel();
714  spinnerThread.join();
715 }
716 
717 TEST(FetchAssetTest, fetchExistingAsset) {
718  auto wsClient = std::make_shared<foxglove::Client<websocketpp::config::asio_client>>();
719  EXPECT_EQ(std::future_status::ready, wsClient->connect(URI).wait_for(DEFAULT_TIMEOUT));
720 
721  const auto millisSinceEpoch = std::chrono::duration_cast<std::chrono::milliseconds>(
722  std::chrono::system_clock::now().time_since_epoch());
723  const auto tmpFilePath =
724  std::filesystem::temp_directory_path() / std::to_string(millisSinceEpoch.count());
725  constexpr char content[] = "Hello, world";
726  FILE* tmpAssetFile = std::fopen(tmpFilePath.c_str(), "w");
727  std::fputs(content, tmpAssetFile);
728  std::fclose(tmpAssetFile);
729 
730  const std::string uri = std::string("file://") + tmpFilePath.string();
731  const uint32_t requestId = 123;
732 
733  auto future = foxglove::waitForFetchAssetResponse(wsClient);
734  wsClient->fetchAsset(uri, requestId);
735  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
736  const foxglove::FetchAssetResponse response = future.get();
737 
738  EXPECT_EQ(response.requestId, requestId);
740  // +1 since NULL terminator is not written to file.
741  ASSERT_EQ(response.data.size() + 1ul, sizeof(content));
742  EXPECT_EQ(0, std::memcmp(content, response.data.data(), response.data.size()));
743  std::remove(tmpFilePath.c_str());
744 }
745 
746 TEST(FetchAssetTest, fetchNonExistingAsset) {
747  auto wsClient = std::make_shared<foxglove::Client<websocketpp::config::asio_client>>();
748  EXPECT_EQ(std::future_status::ready, wsClient->connect(URI).wait_for(DEFAULT_TIMEOUT));
749 
750  const std::string assetId = "file:///foo/bar";
751  const uint32_t requestId = 456;
752 
753  auto future = foxglove::waitForFetchAssetResponse(wsClient);
754  wsClient->fetchAsset(assetId, requestId);
755  ASSERT_EQ(std::future_status::ready, future.wait_for(DEFAULT_TIMEOUT));
756  const foxglove::FetchAssetResponse response = future.get();
757 
758  EXPECT_EQ(response.requestId, requestId);
759  EXPECT_EQ(response.status, foxglove::FetchAssetStatus::Error);
760  EXPECT_FALSE(response.errorMessage.empty());
761 }
762 
763 // Run all the tests that were declared with TEST()
764 int main(int argc, char** argv) {
765  testing::InitGoogleTest(&argc, argv);
766  rclcpp::init(argc, argv);
767 
768  rclcpp::executors::SingleThreadedExecutor executor;
769  rclcpp::NodeOptions nodeOptions;
770  // Explicitly allow file:// asset URIs for testing purposes.
771  nodeOptions.append_parameter_override("asset_uri_allowlist",
772  std::vector<std::string>({"file://.*"}));
773  foxglove_bridge::FoxgloveBridge node(nodeOptions);
774  executor.add_node(node.get_node_base_interface());
775 
776  std::thread spinnerThread([&executor]() {
777  executor.spin();
778  });
779 
780  const auto testResult = RUN_ALL_TESTS();
781 
782  executor.cancel();
783  spinnerThread.join();
784  executor.remove_node(node.get_node_base_interface());
785 
786  return testResult;
787 }
TestWithExecutor
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:26
response
const std::string response
ServiceTest::SERVICE_NAME
static const std::string SERVICE_NAME
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:50
foxglove::Client
Definition: websocket_client.hpp:60
ParameterTest::PARAM_2_TYPE
std::vector< double > PARAM_2_TYPE
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:30
ParameterTest
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:24
serializeMsg
std::shared_ptr< rclcpp::SerializedMessage > serializeMsg(const T *msg)
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:154
ParameterTest::PARAM_1_DEFAULT_VALUE
static const PARAM_1_TYPE PARAM_1_DEFAULT_VALUE
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:28
foxglove::Service::id
ServiceId id
Definition: common.hpp:122
ParameterTest::_paramNode1
rclcpp::Node::SharedPtr _paramNode1
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:99
ParameterTest::PARAM_1_TYPE
std::string PARAM_1_TYPE
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:26
foxglove::ClientAdvertisement::topic
std::string topic
Definition: common.hpp:79
foxglove_bridge::FoxgloveBridge
Definition: ros1_foxglove_bridge_nodelet.cpp:60
foxglove::Channel
Definition: common.hpp:64
STD_MSGS_STRING_SCHEMA
constexpr char STD_MSGS_STRING_SCHEMA[]
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:21
ONE_SECOND
constexpr auto ONE_SECOND
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:23
TEST_F
TEST_F(ParameterTest, testGetAllParams)
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:361
ParameterTest::PARAM_3_TYPE
double PARAM_3_TYPE
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:58
TEST_P
TEST_P(PublisherTest, testPublishing)
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:261
ParameterTest::PARAM_4_TYPE
std::vector< double > PARAM_4_TYPE
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:62
ExistingPublisherTest::_publisher
rclcpp::PublisherBase::SharedPtr _publisher
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:150
foxglove::ServiceResponse::encoding
std::string encoding
Definition: common.hpp:133
foxglove::ParameterType::PARAMETER_ARRAY
@ PARAMETER_ARRAY
ParameterTest::PARAM_1_NAME
static const std::string PARAM_1_NAME
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:27
foxglove::ServiceResponse
Definition: common.hpp:130
main
int main(int argc, char **argv)
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:764
foxglove::ServiceWithoutId::responseSchema
std::string responseSchema
Definition: common.hpp:118
foxglove::ServiceResponse::callId
uint32_t callId
Definition: common.hpp:132
foxglove::FetchAssetResponse
Definition: common.hpp:155
foxglove::Channel::id
ChannelId id
Definition: common.hpp:65
foxglove::waitForServiceResponse
std::future< ServiceResponse > waitForServiceResponse(std::shared_ptr< ClientInterface > client)
Definition: test_client.cpp:51
foxglove::ServiceWithoutId::name
std::string name
Definition: common.hpp:115
PublisherTest
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:129
foxglove::ServiceResponse::data
std::vector< uint8_t > data
Definition: common.hpp:134
foxglove::ServiceWithoutId::type
std::string type
Definition: common.hpp:116
foxglove::waitForChannelMsg
std::future< std::vector< uint8_t > > waitForChannelMsg(ClientInterface *client, SubscriptionId subscriptionId)
Definition: test_client.cpp:11
ServiceTest::SetUp
void SetUp() override
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:109
HELLO_WORLD_CDR
constexpr uint8_t HELLO_WORLD_CDR[]
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:18
ParameterTest::TearDown
void TearDown() override
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:94
deserializeMsg
std::shared_ptr< T > deserializeMsg(const rcl_serialized_message_t *msg)
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:164
ParameterTest::PARAM_4_DEFAULT_VALUE
static const PARAM_4_TYPE PARAM_4_DEFAULT_VALUE
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:64
ParameterTest::PARAM_2_NAME
static const std::string PARAM_2_NAME
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:31
foxglove::ClientAdvertisement::channelId
ClientChannelId channelId
Definition: common.hpp:78
TestWithExecutor::_executorThread
std::thread _executorThread
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:42
DEFAULT_TIMEOUT
constexpr auto DEFAULT_TIMEOUT
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:24
foxglove::Parameter
Definition: parameter.hpp:54
ServiceTest::_node
rclcpp::Node::SharedPtr _node
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:124
HELLO_WORLD_JSON
constexpr char HELLO_WORLD_JSON[]
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:20
TestWithExecutor::~TestWithExecutor
~TestWithExecutor() override
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:34
foxglove::waitForService
std::future< Service > waitForService(std::shared_ptr< ClientInterface > client, const std::string &serviceName)
Definition: test_client.cpp:68
foxglove::ClientAdvertisement
Definition: common.hpp:77
foxglove::json
nlohmann::json json
Definition: websocket_server.hpp:66
TestWithExecutor::TestWithExecutor
TestWithExecutor()
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:28
ParameterTest::NODE_2_NAME
static const std::string NODE_2_NAME
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:54
ServiceTest::_wsClient
std::shared_ptr< foxglove::Client< websocketpp::config::asio_client > > _wsClient
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:126
foxglove::ServiceResponse::serviceId
ServiceId serviceId
Definition: common.hpp:131
foxglove::waitForFetchAssetResponse
std::future< FetchAssetResponse > waitForFetchAssetResponse(std::shared_ptr< ClientInterface > client)
Definition: test_client.cpp:115
foxglove::ClientAdvertisement::schema
std::vector< uint8_t > schema
Definition: common.hpp:82
ParameterTest::DELETABLE_PARAM_NAME
static const std::string DELETABLE_PARAM_NAME
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:51
ExistingPublisherTest::_node
rclcpp::Node::SharedPtr _node
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:149
foxglove::SubscriptionId
uint32_t SubscriptionId
Definition: common.hpp:28
ParameterTest::SetUp
void SetUp() override
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:67
TestWithExecutor::executor
rclcpp::executors::SingleThreadedExecutor executor
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:39
foxglove::waitForParameters
std::future< std::vector< Parameter > > waitForParameters(std::shared_ptr< ClientInterface > client, const std::string &requestId=std::string())
Definition: test_client.cpp:31
INSTANTIATE_TEST_SUITE_P
INSTANTIATE_TEST_SUITE_P(TestPublishingCDR, PublisherTest, testing::Values(std::make_pair("cdr", std::vector< uint8_t >(HELLO_WORLD_CDR, std::end(HELLO_WORLD_CDR)))))
ExistingPublisherTest
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:133
ParameterTest::PARAM_3_NAME
static const std::string PARAM_3_NAME
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:59
ExistingPublisherTest::TOPIC_NAME
static const std::string TOPIC_NAME
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:135
ServiceTest
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:48
foxglove::ServiceWithoutId::requestSchema
std::string requestSchema
Definition: common.hpp:117
foxglove::ClientAdvertisement::encoding
std::string encoding
Definition: common.hpp:80
foxglove::waitForChannel
std::future< Channel > waitForChannel(std::shared_ptr< ClientInterface > client, const std::string &topicName)
Definition: test_client.cpp:92
ExistingPublisherTest::SetUp
void SetUp() override
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:138
foxglove::ClientAdvertisement::schemaName
std::string schemaName
Definition: common.hpp:81
ExistingPublisherTest::TearDown
void TearDown() override
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:145
ParameterTest::NODE_1_NAME
static const std::string NODE_1_NAME
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:48
TEST
TEST(SmokeTest, testConnection)
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:173
foxglove::Client::connect
void connect(const std::string &uri, std::function< void(websocketpp::connection_hdl)> onOpenHandler, std::function< void(websocketpp::connection_hdl)> onCloseHandler=nullptr) override
Definition: websocket_client.hpp:85
ServiceTest::TearDown
void TearDown() override
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:120
ParameterTest::_wsClient
std::shared_ptr< foxglove::Client< websocketpp::config::asio_client > > _wsClient
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:45
ServiceTest::_service
ros::ServiceServer _service
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:65
websocket_client.hpp
URI
constexpr char URI[]
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:15
foxglove::ParameterType::PARAMETER_DOUBLE
@ PARAMETER_DOUBLE
param
T param(const std::string &param_name, const T &default_val)
foxglove::FetchAssetStatus::Error
@ Error
ParameterTest::PARAM_3_DEFAULT_VALUE
static const PARAM_3_TYPE PARAM_3_DEFAULT_VALUE
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:60
test_client.hpp
ParameterTest::_paramNode2
rclcpp::Node::SharedPtr _paramNode2
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:100
ParameterTest::PARAM_4_NAME
static const std::string PARAM_4_NAME
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:63
time_since_epoch
double time_since_epoch()
ros2_foxglove_bridge.hpp
ParameterTest::PARAM_2_DEFAULT_VALUE
static const PARAM_2_TYPE PARAM_2_DEFAULT_VALUE
Definition: ros1_foxglove_bridge/tests/smoke_test.cpp:32
ServiceTest::_service
rclcpp::ServiceBase::SharedPtr _service
Definition: ros2_foxglove_bridge/tests/smoke_test.cpp:125
foxglove::FetchAssetStatus::Success
@ Success
foxglove::Service
Definition: common.hpp:121


foxglove_bridge
Author(s): Foxglove
autogenerated on Wed Mar 5 2025 03:34:31