CARLA
Loading...
Searching...
No Matches
test_streaming.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2// de Barcelona (UAB).
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
7#include "test.h"
8
9#include <carla/ThreadGroup.h>
17
18#include <boost/asio/executor_work_guard.hpp>
19
20#include <atomic>
21#include <chrono>
22
23using namespace std::chrono_literals;
24
25// This is required for low level to properly stop the threads in case of
26// exception/assert.
28public:
29
30 boost::asio::io_context service;
31
32 explicit io_context_running(size_t threads = 2u)
33 : _work_to_do(boost::asio::make_work_guard(service)) {
34 _threads.CreateThreads(threads, [this]() { service.run(); });
35 }
36
38 stop();
39 }
40
41 void stop() {
42 service.stop();
44 }
45
46private:
47
48 boost::asio::executor_work_guard<boost::asio::io_context::executor_type> _work_to_do;
49
51};
52
53TEST(streaming, low_level_sending_strings) {
54 using namespace util::buffer;
55 using namespace carla::streaming;
56 using namespace carla::streaming::detail;
57 using namespace carla::streaming::low_level;
58
59 constexpr auto number_of_messages = 100u;
60 const std::string message_text = "Hello client!";
61
62 std::atomic_size_t message_count{0u};
63
65
67 srv.SetTimeout(1s);
68
69 auto stream = srv.MakeStream();
70
72 c.Subscribe(io.service, stream.token(), [&](auto message) {
73 ++message_count;
74 ASSERT_EQ(message.size(), message_text.size());
75 const std::string msg = as_string(message);
76 ASSERT_EQ(msg, message_text);
77 });
78
79 carla::Buffer Buf(boost::asio::buffer(message_text.c_str(), message_text.size()));
81 for (auto i = 0u; i < number_of_messages; ++i) {
82 std::this_thread::sleep_for(2ms);
83 carla::SharedBufferView View = BufView;
84 stream.Write(View);
85 }
86
87 std::this_thread::sleep_for(2ms);
88 ASSERT_GE(message_count, number_of_messages - 3u);
89
90 io.stop();
91}
92
93TEST(streaming, low_level_unsubscribing) {
94 using namespace util::buffer;
95 using namespace carla::streaming;
96 using namespace carla::streaming::detail;
97 using namespace carla::streaming::low_level;
98
99 constexpr auto number_of_messages = 50u;
100 const std::string message_text = "Hello client!";
101
103
105 srv.SetTimeout(1s);
106
108 for (auto n = 0u; n < 10u; ++n) {
109 auto stream = srv.MakeStream();
110 std::atomic_size_t message_count{0u};
111
112 c.Subscribe(io.service, stream.token(), [&](auto message) {
113 ++message_count;
114 ASSERT_EQ(message.size(), message_text.size());
115 const std::string msg = as_string(message);
116 ASSERT_EQ(msg, message_text);
117 });
118
119 carla::Buffer Buf(boost::asio::buffer(message_text.c_str(), message_text.size()));
121 for (auto i = 0u; i < number_of_messages; ++i) {
122 std::this_thread::sleep_for(4ms);
123 carla::SharedBufferView View = BufView;
124 stream.Write(View);
125 }
126
127 std::this_thread::sleep_for(4ms);
128 c.UnSubscribe(stream.token());
129
130 for (auto i = 0u; i < number_of_messages; ++i) {
131 std::this_thread::sleep_for(2ms);
132 carla::SharedBufferView View = BufView;
133 stream.Write(View);
134 }
135
136 ASSERT_GE(message_count, number_of_messages - 3u);
137 }
138
139 io.stop();
140}
141
142TEST(streaming, low_level_tcp_small_message) {
143 using namespace carla::streaming;
144 using namespace carla::streaming::detail;
145
146 boost::asio::io_context io_context;
147 tcp::Server::endpoint ep(boost::asio::ip::tcp::v4(), TESTING_PORT);
148
149 tcp::Server srv(io_context, ep);
150 srv.SetTimeout(1s);
151 std::atomic_bool done{false};
152 std::atomic_size_t message_count{0u};
153
154 const std::string msg = "Hola!";
155
156 srv.Listen([&](std::shared_ptr<tcp::ServerSession> session) {
157 ASSERT_EQ(session->get_stream_id(), 1u);
158
159 carla::Buffer Buf(boost::asio::buffer(msg.c_str(), msg.size()));
161 while (!done) {
162 std::this_thread::sleep_for(1ns);
163 carla::SharedBufferView View = BufView;
164 session->Write(View);
165 }
166 std::cout << "done!\n";
167 }, [](std::shared_ptr<tcp::ServerSession>) { std::cout << "session closed!\n"; });
168
169 // Use loopback: Windows refuses to connect to the 0.0.0.0 wildcard the server binds to.
170 Dispatcher dispatcher{make_endpoint<tcp::Client::protocol_type>(
171 {make_localhost_address(), srv.GetLocalEndpoint().port()})};
172 auto stream = dispatcher.MakeStream();
173 auto c = std::make_shared<tcp::Client>(io_context, stream.token(), [&](carla::Buffer message) {
174 ++message_count;
175 ASSERT_FALSE(message.empty());
176 ASSERT_EQ(message.size(), 5u);
177 const std::string received = util::buffer::as_string(message);
178 ASSERT_EQ(received, msg);
179 });
180 c->Connect();
181
182 // We need at least two threads because this server loop consumes one.
183 carla::ThreadGroup threads;
184 threads.CreateThreads(
185 std::max(2u, std::thread::hardware_concurrency()),
186 [&]() { io_context.run(); });
187
188 std::this_thread::sleep_for(2s);
189 io_context.stop();
190 done = true;
191 std::cout << "client received " << message_count << " messages\n";
192 ASSERT_GT(message_count, 10u);
193 c->Stop();
194}
195
196// A callback that throws, such as an RPC timing out in the light refresh after
197// a map change (#10165), used to escape io_context::run() and call
198// std::terminate. The client now logs it and keeps reading the stream.
199TEST(streaming, low_level_tcp_callback_exception_does_not_terminate) {
200 using namespace carla::streaming;
201 using namespace carla::streaming::detail;
202
203 boost::asio::io_context io_context;
204 tcp::Server::endpoint ep(boost::asio::ip::tcp::v4(), TESTING_PORT);
205
206 tcp::Server srv(io_context, ep);
207 srv.SetTimeout(1s);
208 std::atomic_bool done{false};
209 std::atomic_size_t message_count{0u};
210
211 const std::string msg = "Hola!";
212
213 srv.Listen([&](std::shared_ptr<tcp::ServerSession> session) {
214 carla::Buffer Buf(boost::asio::buffer(msg.c_str(), msg.size()));
216 while (!done) {
217 std::this_thread::sleep_for(1ns);
218 carla::SharedBufferView View = BufView;
219 session->Write(View);
220 }
221 }, [](std::shared_ptr<tcp::ServerSession>) {});
222
223 // Use loopback: Windows refuses to connect to the 0.0.0.0 wildcard the server binds to.
224 Dispatcher dispatcher{make_endpoint<tcp::Client::protocol_type>(
225 {make_localhost_address(), srv.GetLocalEndpoint().port()})};
226 auto stream = dispatcher.MakeStream();
227 auto c = std::make_shared<tcp::Client>(io_context, stream.token(), [&](carla::Buffer) {
228 if (++message_count == 1u) {
229 throw std::runtime_error("callback failure on the first message");
230 }
231 });
232 c->Connect();
233
234 // We need at least two threads because this server loop consumes one.
235 carla::ThreadGroup threads;
236 threads.CreateThreads(
237 std::max(2u, std::thread::hardware_concurrency()),
238 [&]() { io_context.run(); });
239
240 std::this_thread::sleep_for(2s);
241 io_context.stop();
242 done = true;
243 std::cout << "client received " << message_count << " messages\n";
244 ASSERT_GT(message_count, 10u);
245 c->Stop();
246}
247
248struct DoneGuard {
249 ~DoneGuard() { done = true; };
250 std::atomic_bool &done;
251};
252
253TEST(streaming, stream_outlives_server) {
254 using namespace carla::streaming;
255 using namespace util::buffer;
256 constexpr size_t iterations = 10u;
257 std::atomic_bool done{false};
258 const std::string message = "Hello client, how are you?";
259 std::shared_ptr<Stream> stream;
260
261 carla::ThreadGroup sender;
262 DoneGuard g = {done};
263 sender.CreateThread([&]() {
264
265 carla::Buffer Buf(boost::asio::buffer(message.c_str(), message.size()));
267 while (!done) {
268 // sleep_for(1ms) takes ~15 ms on Windows, too slow for the client's 20 ms window.
269 const auto next_write = std::chrono::steady_clock::now() + 1ms;
270 while (!done && std::chrono::steady_clock::now() < next_write) {
271 std::this_thread::yield();
272 }
273 auto s = std::atomic_load_explicit(&stream, std::memory_order_relaxed);
274 if (s != nullptr) {
275 carla::SharedBufferView View = BufView;
276 s->Write(View);
277 }
278 }
279 });
280
281 for (auto i = 0u; i < iterations; ++i) {
282 Server srv(TESTING_PORT);
283 srv.AsyncRun(2u);
284 {
285 auto s = std::make_shared<Stream>(srv.MakeStream());
286 std::atomic_store_explicit(&stream, s, std::memory_order_relaxed);
287 }
288 std::atomic_size_t messages_received{0u};
289 {
290 Client c;
291 c.AsyncRun(2u);
292 c.Subscribe(stream->token(), [&](auto buffer) {
293 const std::string result = as_string(buffer);
294 ASSERT_EQ(result, message);
295 ++messages_received;
296 });
297 std::this_thread::sleep_for(20ms);
298 } // client dies here.
299 ASSERT_GT(messages_received, 0u);
300 // Detach the sender from this iteration's stream before the server tears
301 // down. Otherwise the sender can call Session::Write on a session whose
302 // io_context is being stopped inside ~Server, which segfaults
303 // intermittently in Release builds on slow runners.
304 std::atomic_store_explicit(&stream, std::shared_ptr<Stream>(), std::memory_order_relaxed);
305 std::this_thread::sleep_for(20ms);
306 } // server dies here.
307 done = true;
308} // stream dies here.
309
310TEST(streaming, multi_stream) {
311 using namespace carla::streaming;
312 using namespace util::buffer;
313 constexpr size_t number_of_messages = 100u;
314 constexpr size_t number_of_clients = 6u;
315 constexpr size_t iterations = 10u;
316 const std::string message = "Hi y'all!";
317
318 Server srv(TESTING_PORT);
319 srv.AsyncRun(number_of_clients);
320 auto stream = srv.MakeStream();
321
322 for (auto i = 0u; i < iterations; ++i) {
323 std::vector<std::pair<std::atomic_size_t, std::unique_ptr<Client>>> v(number_of_clients);
324
325 for (auto &pair : v) {
326 pair.first = 0u;
327 pair.second = std::make_unique<Client>();
328 pair.second->AsyncRun(1u);
329 pair.second->Subscribe(stream.token(), [&](auto buffer) {
330 const std::string result = as_string(buffer);
331 ASSERT_EQ(result, message);
332 ++pair.first;
333 });
334 }
335
336 carla::Buffer Buf(boost::asio::buffer(message.c_str(), message.size()));
338 std::this_thread::sleep_for(6ms);
339 for (auto j = 0u; j < number_of_messages; ++j) {
340 std::this_thread::sleep_for(6ms);
341 carla::SharedBufferView View = BufView;
342 stream.Write(View);
343 }
344 std::this_thread::sleep_for(6ms);
345
346 for (auto &pair : v) {
347 ASSERT_GE(pair.first, number_of_messages - 3u);
348 }
349 }
350}
351
352// When the ROS2 topic-visibility default is left off (the out-of-the-box
353// behavior), a freshly created stream is not visible to ROS 2 until something
354// explicitly enables it.
355TEST(streaming, ros_topic_visibility_default_off) {
356 using namespace carla::streaming;
357 using namespace carla::streaming::detail;
358
359 Server srv(TESTING_PORT);
360 constexpr stream_id_type sensor_id{1u};
361 srv.GetToken(sensor_id); // Materializes the stream in the dispatcher.
362 EXPECT_FALSE(srv.IsEnabledForROS(sensor_id));
363}
364
365// With the default flag on, every stream created afterwards is ROS-visible from
366// the moment it is born (this is what ROS2TopicVisibility=true delivers).
367TEST(streaming, ros_topic_visibility_default_on) {
368 using namespace carla::streaming;
369 using namespace carla::streaming::detail;
370
371 Server srv(TESTING_PORT);
373 constexpr stream_id_type sensor_id{1u};
374 srv.GetToken(sensor_id);
375 EXPECT_TRUE(srv.IsEnabledForROS(sensor_id));
376}
377
378// The default flag must also apply to streams born through MakeStream(), not
379// only through the GetToken() create-on-miss path.
380TEST(streaming, ros_topic_visibility_make_stream_path) {
381 using namespace carla::streaming;
382 using namespace carla::streaming::detail;
383
384 Server srv(TESTING_PORT);
386 auto stream = srv.MakeStream();
387 const auto sensor_id = token_type(stream.token()).get_stream_id();
388 EXPECT_TRUE(srv.IsEnabledForROS(sensor_id));
389}
390
391// Independently of the startup default, the per-stream EnableForROS /
392// DisableForROS surface this feature relies on must round-trip.
393TEST(streaming, ros_topic_visibility_manual_toggle) {
394 using namespace carla::streaming;
395 using namespace carla::streaming::detail;
396
397 Server srv(TESTING_PORT);
398 constexpr stream_id_type sensor_id{1u};
399 srv.GetToken(sensor_id);
400 EXPECT_FALSE(srv.IsEnabledForROS(sensor_id));
401 srv.EnableForROS(sensor_id);
402 EXPECT_TRUE(srv.IsEnabledForROS(sensor_id));
403 srv.DisableForROS(sensor_id);
404 EXPECT_FALSE(srv.IsEnabledForROS(sensor_id));
405}
static std::shared_ptr< BufferView > CreateFrom(Buffer &&buffer)
Definition BufferView.h:56
A piece of raw data.
void CreateThreads(size_t count, F functor)
Definition ThreadGroup.h:32
void CreateThread(F &&functor)
Definition ThreadGroup.h:27
A client able to subscribe to multiple streams.
void AsyncRun(size_t worker_threads)
void Subscribe(const Token &token, Functor &&callback)
A streaming server.
token_type GetToken(stream_id sensor_id)
bool IsEnabledForROS(stream_id sensor_id)
void EnableForROS(stream_id sensor_id)
void SetROS2TopicVisibilityDefaultEnabled(bool enabled)
void AsyncRun(size_t worker_threads)
void DisableForROS(stream_id sensor_id)
Keeps the mapping between streams and sessions.
Definition Dispatcher.h:28
carla::streaming::Stream MakeStream()
void SetTimeout(time_duration timeout)
Set session time-out.
void Listen(FunctorT1 on_session_opened, FunctorT2 on_session_closed)
Start listening for connections.
Serializes a stream endpoint.
A client able to subscribe to multiple streams.
void Subscribe(boost::asio::io_context &io_context, token_type token, Functor &&callback)
boost::asio::executor_work_guard< boost::asio::io_context::executor_type > _work_to_do
boost::asio::io_context service
carla::ThreadGroup _threads
io_context_running(size_t threads=2u)
std::shared_ptr< BufferView > SharedBufferView
Definition BufferView.h:151
std::atomic_bool & done
constexpr uint16_t TESTING_PORT
Definition test.h:24
TEST(streaming, low_level_sending_strings)