CIRCT 24.0.0git
Loading...
Searching...
No Matches
ESIRuntimeCosimRpcTest.cpp
Go to the documentation of this file.
1//===- ESIRuntimeCosimRpcTest.cpp - Cosim RPC protocol tests --------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// Runs the cosim RpcServer and RpcClient in-process (no simulator) to check
10// the protocol's behavior.
11//
12//===----------------------------------------------------------------------===//
13
14#include "esi/Context.h"
15#include "esi/Logging.h"
16#include "esi/Ports.h"
19#include "gtest/gtest.h"
20
21#include <atomic>
22#include <chrono>
23#include <cstdint>
24#include <filesystem>
25#include <functional>
26#include <future>
27#include <memory>
28#include <mutex>
29#include <thread>
30#include <utility>
31#include <vector>
32
33using namespace esi;
36using namespace std::chrono_literals;
37
38namespace {
39
40MessageData msg(uint32_t v) { return MessageData::from(v); }
41
42/// Poll `cond` until it holds or a generous deadline passes.
43bool waitFor(const std::function<bool()> &cond,
44 std::chrono::milliseconds timeout = 10s) {
45 auto deadline = std::chrono::steady_clock::now() + timeout;
46 while (!cond()) {
47 if (std::chrono::steady_clock::now() > deadline)
48 return false;
49 std::this_thread::sleep_for(1ms);
50 }
51 return true;
52}
53
54class CosimRpcTest : public ::testing::Test {
55protected:
56 void SetUp() override {
57 // RpcServer writes `cosim.cfg` into the working directory.
58 oldCwd = std::filesystem::current_path();
59 tmpDir = std::filesystem::temp_directory_path() /
60 ("esi_cosim_rpc_test_" +
61 std::to_string(
62 std::chrono::steady_clock::now().time_since_epoch().count()));
63 std::filesystem::create_directories(tmpDir);
64 std::filesystem::current_path(tmpDir);
65
66 ctxt = Context::withLogger<NullLogger>();
67 server = std::make_unique<RpcServer>(*ctxt);
68 // The server picks a free port by probing, then binds it; another process
69 // can take it in between. Retry, since tests run in parallel.
70 for (int attempt = 0;; ++attempt) {
71 try {
72 server->run(0);
73 break;
74 } catch (const std::runtime_error &) {
75 if (attempt == 4)
76 throw;
77 }
78 }
79 toServer = &server->registerReadPort("in", "i32");
80 // Registered, but left for each test to connect (or not).
81 lateToServer = &server->registerReadPort("late", "i32");
82 toClient = &server->registerWritePort("out", "i32");
83 toServer->connect();
84 toClient->connect();
85 // Non-empty: IXWebSocket's base64 encoder crashes on empty input.
86 server->setManifest(0, {1, 2, 3, 4});
87 }
88
89 void TearDown() override {
90 client.reset();
91 if (server)
92 server->stop();
93 server.reset();
94 std::filesystem::current_path(oldCwd);
95 std::error_code ec;
96 std::filesystem::remove_all(tmpDir, ec);
97 }
98
99 void connectClient() {
100 client = std::make_unique<RpcClient>(ctxt->getLogger(), "127.0.0.1",
101 server->getPort());
102 }
103
104 /// Read one message from the to-server port as the simulator would, and
105 /// return its credit.
106 uint32_t consumeOne() { return consumeFrom(*toServer); }
107
108 std::filesystem::path oldCwd, tmpDir;
109 std::unique_ptr<Context> ctxt;
110 std::unique_ptr<RpcServer> server;
111 ReadChannelPort *toServer = nullptr;
112 ReadChannelPort *lateToServer = nullptr;
113 WriteChannelPort *toClient = nullptr;
114 std::unique_ptr<RpcClient> client;
115
116 /// Read one message from `port` as the simulator would, and return its
117 /// credit. Fails the test (and returns an arbitrary value) rather than
118 /// hanging if none arrives.
119 uint32_t consumeFrom(ReadChannelPort &port) {
120 std::future<MessageData> f = port.readAsync();
121 if (f.wait_for(10s) != std::future_status::ready) {
122 ADD_FAILURE() << "timed out waiting for a message";
123 return ~0u;
124 }
125 MessageData m = f.get();
126 server->messageConsumed(port);
127 return *m.as<uint32_t>();
128 }
129};
130
131TEST_F(CosimRpcTest, ToServerTryWriteBackpressure) {
132 connectClient();
133 client->setToServerMaxInFlight("in", 4);
134
135 for (uint32_t i = 0; i < 4; ++i)
136 EXPECT_TRUE(client->tryWriteToServer("in", {msg(i)}));
137 // At the in-flight limit: nothing consumed yet.
138 EXPECT_FALSE(client->tryWriteToServer("in", {msg(4)}));
139
140 // Consuming one returns one credit.
141 EXPECT_EQ(consumeOne(), 0u);
142 ASSERT_TRUE(
143 waitFor([&] { return client->tryWriteToServer("in", {msg(4)}); }));
144 EXPECT_FALSE(client->tryWriteToServer("in", {msg(5)}));
145
146 for (uint32_t i = 1; i <= 4; ++i)
147 EXPECT_EQ(consumeOne(), i);
148
149 // A multi-frame message is admitted on a single credit.
150 ASSERT_TRUE(waitFor([&] {
151 return client->tryWriteToServer(
152 "in", {msg(10), msg(11), msg(12), msg(13), msg(14)});
153 }));
154 EXPECT_FALSE(client->tryWriteToServer("in", {msg(15)}));
155 for (uint32_t i = 10; i <= 14; ++i)
156 EXPECT_EQ(consumeOne(), i);
157
158 // With no in-flight limit, writes are never refused.
159 client->setToServerMaxInFlight("in", 0);
160 for (uint32_t i = 0; i < 100; ++i)
161 EXPECT_TRUE(client->tryWriteToServer("in", {msg(i)}));
162 for (uint32_t i = 0; i < 100; ++i)
163 EXPECT_EQ(consumeOne(), i);
164}
165
166TEST_F(CosimRpcTest, ToServerForcedWriteIgnoresLimit) {
167 connectClient();
168 client->setToServerMaxInFlight("in", 1);
169 EXPECT_TRUE(client->tryWriteToServer("in", {msg(0)}));
170 EXPECT_FALSE(client->tryWriteToServer("in", {msg(1)}));
171 // The rest of an admitted message is sent regardless of the limit, and
172 // counts as in flight.
173 EXPECT_TRUE(client->tryWriteToServer("in", {msg(1)}, /*force=*/true));
174 EXPECT_EQ(consumeOne(), 0u);
175 std::this_thread::sleep_for(50ms);
176 EXPECT_FALSE(client->tryWriteToServer("in", {msg(2)}));
177 EXPECT_EQ(consumeOne(), 1u);
178 ASSERT_TRUE(
179 waitFor([&] { return client->tryWriteToServer("in", {msg(2)}); }));
180 EXPECT_EQ(consumeOne(), 2u);
181}
182
183TEST_F(CosimRpcTest, ToServerBlockingWrite) {
184 connectClient();
185 client->setToServerMaxInFlight("in", 2);
186 client->writeToServer("in", msg(0));
187 client->writeToServer("in", msg(1));
188
189 std::atomic<bool> done = false;
190 std::thread writer([&] {
191 client->writeToServer("in", msg(2));
192 done = true;
193 });
194 std::this_thread::sleep_for(100ms);
195 EXPECT_FALSE(done.load()) << "write() should block at the in-flight limit";
196
197 EXPECT_EQ(consumeOne(), 0u);
198 EXPECT_TRUE(waitFor([&] { return done.load(); }));
199 writer.join();
200 EXPECT_EQ(consumeOne(), 1u);
201 EXPECT_EQ(consumeOne(), 2u);
202}
203
204TEST_F(CosimRpcTest, ToServerBlockedWriterThrowsOnDisconnect) {
205 connectClient();
206 // Use a port the simulator side never connects: frames held for it stay in
207 // flight even when the server stops. (Stopping disconnects connected ports,
208 // which hands back the in-flight count of frames they held, legitimately
209 // unblocking the writer.)
210 client->setToServerMaxInFlight("late", 1);
211 client->writeToServer("late", msg(0));
212
213 std::promise<bool> threw;
214 std::thread writer([&] {
215 try {
216 client->writeToServer("late", msg(1));
217 threw.set_value(false);
218 } catch (const std::exception &) {
219 threw.set_value(true);
220 }
221 });
222 std::this_thread::sleep_for(50ms);
223 server->stop();
224 server.reset();
225 auto f = threw.get_future();
226 ASSERT_EQ(f.wait_for(10s), std::future_status::ready);
227 EXPECT_TRUE(f.get());
228 writer.join();
229}
230
231TEST_F(CosimRpcTest, ToClientBackpressure) {
232 // Without a subscriber, the default in-flight limit bounds the server-side
233 // queue.
234 uint32_t next = 0;
235 while (toClient->tryWrite(msg(next)))
236 ASSERT_LT(++next, 1000u) << "server never applied backpressure";
238
239 connectClient();
240 std::atomic<bool> accepting = true;
241 std::mutex receivedMutex;
242 std::vector<uint32_t> received;
243 auto conn = client->connectClientReceiver(
244 "out",
245 [&](std::unique_ptr<SegmentedMessageData> &data) {
246 if (!accepting)
247 return false;
248 std::lock_guard<std::mutex> lock(receivedMutex);
249 received.push_back(*data->toMessageData().as<uint32_t>());
250 return true;
251 },
252 /*maxInFlight=*/4);
253 auto numReceived = [&] {
254 std::lock_guard<std::mutex> lock(receivedMutex);
255 return received.size();
256 };
257
258 // The backlog drains to the client, whose acks make room again.
259 ASSERT_TRUE(waitFor([&] { return numReceived() == next; }));
260 ASSERT_TRUE(waitFor([&] { return toClient->tryWrite(msg(next)); }));
261 ++next;
262 ASSERT_TRUE(waitFor([&] { return numReceived() == next; }));
263
264 // While the client refuses messages, at most `maxInFlight` can be in
265 // flight.
266 accepting = false;
267 uint32_t admitted = 0;
268 while (toClient->tryWrite(msg(next))) {
269 ++next;
270 ASSERT_LE(++admitted, 4u);
271 }
272 std::this_thread::sleep_for(100ms);
273 EXPECT_FALSE(toClient->tryWrite(msg(next)));
274
275 // Once the client consumes again, credits flow back.
276 accepting = true;
277 ASSERT_TRUE(waitFor([&] { return toClient->tryWrite(msg(next)); }));
278 ++next;
279 ASSERT_TRUE(waitFor([&] { return numReceived() == next; }));
280 std::lock_guard<std::mutex> lock(receivedMutex);
281 for (uint32_t i = 0; i < next; ++i)
282 EXPECT_EQ(received[i], i);
283}
284
285TEST_F(CosimRpcTest, ToClientUnsubscribeResetsInFlight) {
286 connectClient();
287 {
288 std::atomic<bool> seen = false;
289 auto conn = client->connectClientReceiver(
290 "out",
291 [&](std::unique_ptr<SegmentedMessageData> &) {
292 seen = true;
293 return false;
294 },
295 /*maxInFlight=*/1);
296 EXPECT_TRUE(toClient->tryWrite(msg(0)));
297 EXPECT_FALSE(toClient->tryWrite(msg(1)));
298 // Make sure it was sent (not merely queued, which the server would keep
299 // for the next subscriber) before unsubscribing.
300 ASSERT_TRUE(waitFor([&] { return seen.load(); }));
301 }
302 // Unsubscribing drops the unacknowledged message and restores the default
303 // in-flight limit for the (now empty) queue.
304 uint32_t admitted = 0;
305 ASSERT_TRUE(waitFor([&] { return toClient->tryWrite(msg(0)); }));
306 ++admitted;
307 while (toClient->tryWrite(msg(admitted)))
308 ASSERT_LE(++admitted, RpcClient::DefaultToClientMaxInFlight);
309 EXPECT_EQ(admitted, RpcClient::DefaultToClientMaxInFlight);
310}
311
312TEST_F(CosimRpcTest, UnconnectedServerPortBackpressures) {
313 connectClient();
314 // Frames for the unconnected port must be parked and count as in flight,
315 // so the client stalls rather than losing data.
316 client->setToServerMaxInFlight("late", 4);
317 for (uint32_t i = 0; i < 4; ++i)
318 EXPECT_TRUE(client->tryWriteToServer("late", {msg(i)}));
319 std::this_thread::sleep_for(50ms);
320 EXPECT_FALSE(client->tryWriteToServer("late", {msg(4)}));
321
322 // Connecting hands the parked frames over in order.
323 lateToServer->connect();
324 for (uint32_t i = 0; i < 4; ++i)
325 EXPECT_EQ(consumeFrom(*lateToServer), i);
326 ASSERT_TRUE(
327 waitFor([&] { return client->tryWriteToServer("late", {msg(4)}); }));
328 EXPECT_EQ(consumeFrom(*lateToServer), 4u);
329}
330
331TEST_F(CosimRpcTest, ServerPortDisconnectReturnsInFlight) {
332 connectClient();
333 lateToServer->connect();
334 client->setToServerMaxInFlight("late", 3);
335 for (uint32_t i = 0; i < 3; ++i)
336 EXPECT_TRUE(client->tryWriteToServer("late", {msg(i)}));
337 // Make sure they reached the port's queue before disconnecting: the server
338 // handles frames in arrival order, so once a later frame on another
339 // channel has been delivered, these have too.
340 client->writeToServer("in", msg(99));
341 EXPECT_EQ(consumeOne(), 99u);
342
343 // Disconnecting discards the unconsumed frames; their count comes back.
344 lateToServer->disconnect();
345 ASSERT_TRUE(
346 waitFor([&] { return client->tryWriteToServer("late", {msg(10)}); }));
347
348 // And frames sent while disconnected are parked until reconnect.
349 lateToServer->connect();
350 EXPECT_EQ(consumeFrom(*lateToServer), 10u);
351}
352
353TEST_F(CosimRpcTest, RefusedFramesDontSkewDisconnectCredits) {
354 connectClient();
355 client->setToServerMaxInFlight("late", 1);
356 // A callback-mode consumer which refuses everything: frames are dropped
357 // and their credits returned.
358 std::atomic<int> offered{0};
359 lateToServer->connect([&](MessageData) -> bool {
360 ++offered;
361 return false;
362 });
363 EXPECT_TRUE(client->tryWriteToServer("late", {msg(1)}));
364 ASSERT_TRUE(
365 waitFor([&] { return client->tryWriteToServer("late", {msg(2)}); }));
366 ASSERT_TRUE(waitFor([&] { return offered.load() == 2; }));
367 lateToServer->disconnect();
368
369 // Reconnect in polling mode and leave one frame unread.
370 lateToServer->connect();
371 ASSERT_TRUE(
372 waitFor([&] { return client->tryWriteToServer("late", {msg(3)}); }));
373 client->writeToServer("in", msg(99));
374 EXPECT_EQ(consumeOne(), 99u);
375 EXPECT_FALSE(client->tryWriteToServer("late", {msg(4)}));
376
377 // Disconnecting discards it, which must return its credit.
378 lateToServer->disconnect();
379 ASSERT_TRUE(
380 waitFor([&] { return client->tryWriteToServer("late", {msg(5)}); }));
381}
382
383TEST_F(CosimRpcTest, DepartedClientsCreditsNotGivenToNext) {
384 // A client leaves a frame parked on "late" and goes away.
385 connectClient();
386 client->writeToServer("late", msg(1));
387 client->writeToServer("in", msg(99));
388 EXPECT_EQ(consumeOne(), 99u);
389 client.reset();
390
391 // The next client reaches its in-flight limit on "late".
392 connectClient();
393 client->setToServerMaxInFlight("late", 1);
394 EXPECT_TRUE(client->tryWriteToServer("late", {msg(2)}));
395 client->writeToServer("in", msg(98));
396 EXPECT_EQ(consumeOne(), 98u);
397
398 // Consuming the departed client's frame returns nothing to this one.
399 lateToServer->connect();
400 EXPECT_EQ(consumeFrom(*lateToServer), 1u);
401 std::this_thread::sleep_for(100ms);
402 EXPECT_FALSE(client->tryWriteToServer("late", {msg(3)}));
403
404 // Consuming its own frame does.
405 EXPECT_EQ(consumeFrom(*lateToServer), 2u);
406 ASSERT_TRUE(
407 waitFor([&] { return client->tryWriteToServer("late", {msg(3)}); }));
408 EXPECT_EQ(consumeFrom(*lateToServer), 3u);
409}
410
411TEST_F(CosimRpcTest, UnconnectedServerPortHoldsFrames) {
412 connectClient();
413 // Frames for a port the simulator hasn't connected yet are held, not
414 // dropped, and handed over in order once it connects.
415 for (uint32_t i = 0; i < 4; ++i)
416 client->writeToServer("late", msg(i));
417 // Make sure they've arrived: the server handles frames in order, so once a
418 // later frame on another channel has been delivered, these have too.
419 client->writeToServer("in", msg(99));
420 EXPECT_EQ(consumeOne(), 99u);
421
422 lateToServer->connect();
423 for (uint32_t i = 0; i < 4; ++i)
424 EXPECT_EQ(consumeFrom(*lateToServer), i);
425
426 // And again after a disconnect.
427 lateToServer->disconnect();
428 client->writeToServer("late", msg(10));
429 client->writeToServer("in", msg(98));
430 EXPECT_EQ(consumeOne(), 98u);
431 lateToServer->connect();
432 EXPECT_EQ(consumeFrom(*lateToServer), 10u);
433}
434
435TEST_F(CosimRpcTest, RejectedClientLeavesActiveOneConnected) {
436 connectClient();
437 // Shared, since the callback may still be running in `set_value()` once
438 // the test has seen the result and is returning.
439 auto received = std::make_shared<std::promise<uint32_t>>();
440 std::future<uint32_t> f = received->get_future();
441 auto conn = client->connectClientReceiver(
442 "out", [received](std::unique_ptr<SegmentedMessageData> &data) {
443 received->set_value(*data->toMessageData().as<uint32_t>());
444 return true;
445 });
446
447 // Only one client at a time: a second is turned away...
448 EXPECT_ANY_THROW(
449 RpcClient(ctxt->getLogger(), "127.0.0.1", server->getPort()));
450 // (Let the server see it close.)
451 std::this_thread::sleep_for(100ms);
452
453 // ...without disturbing the first.
454 toClient->write(msg(7));
455 ASSERT_EQ(f.wait_for(10s), std::future_status::ready);
456 EXPECT_EQ(f.get(), 7u);
457}
458
459TEST_F(CosimRpcTest, RefusedParkedFrameDoesNotBlockTheRest) {
460 connectClient();
461 for (uint32_t i = 0; i < 3; ++i)
462 client->writeToServer("late", msg(i));
463 client->writeToServer("in", msg(99));
464 EXPECT_EQ(consumeOne(), 99u);
465
466 // A callback-mode consumer which refuses the first frame it's offered.
467 std::mutex receivedMutex;
468 std::vector<uint32_t> received;
469 bool refused = false;
470 lateToServer->connect([&](MessageData data) -> bool {
471 std::lock_guard<std::mutex> lock(receivedMutex);
472 if (!refused) {
473 refused = true;
474 return false;
475 }
476 received.push_back(*data.as<uint32_t>());
477 return true;
478 });
479 // The refused frame is dropped, as one arriving while connected would be;
480 // the ones behind it still get through.
481 {
482 std::lock_guard<std::mutex> lock(receivedMutex);
483 EXPECT_EQ(received, (std::vector<uint32_t>{1, 2}));
484 }
485 // The callback refers to locals.
486 lateToServer->disconnect();
487}
488
489TEST_F(CosimRpcTest, SecondReceiverRejected) {
490 connectClient();
491 // Shared, since the callback may still be running in `set_value()` once
492 // the test has seen the result and is returning.
493 auto received = std::make_shared<std::promise<uint32_t>>();
494 std::future<uint32_t> f = received->get_future();
495 auto conn = client->connectClientReceiver(
496 "out", [received](std::unique_ptr<SegmentedMessageData> &data) {
497 received->set_value(*data->toMessageData().as<uint32_t>());
498 return true;
499 });
500 EXPECT_THROW(
501 client->connectClientReceiver(
502 "out", [](std::unique_ptr<SegmentedMessageData> &) { return true; }),
503 std::runtime_error);
504
505 // The first receiver is undisturbed.
506 toClient->write(msg(5));
507 ASSERT_EQ(f.wait_for(10s), std::future_status::ready);
508 EXPECT_EQ(f.get(), 5u);
509}
510
511TEST_F(CosimRpcTest, ToClientBlockingWrite) {
512 connectClient();
513 std::atomic<bool> accepting = false;
514 std::atomic<int> received = 0;
515 auto conn = client->connectClientReceiver(
516 "out",
517 [&](std::unique_ptr<SegmentedMessageData> &) {
518 if (!accepting)
519 return false;
520 ++received;
521 return true;
522 },
523 /*maxInFlight=*/2);
524 toClient->write(msg(0));
525 toClient->write(msg(1));
526
527 // At the limit: `write()` blocks until the client consumes something.
528 std::atomic<bool> written = false;
529 std::thread writer([&] {
530 toClient->write(msg(2));
531 written = true;
532 });
533 std::this_thread::sleep_for(100ms);
534 EXPECT_FALSE(written);
535 accepting = true;
536 ASSERT_TRUE(waitFor([&] { return written.load(); }));
537 writer.join();
538 ASSERT_TRUE(waitFor([&] { return received == 3; }));
539}
540
541TEST_F(CosimRpcTest, ToClientBlockedWriteThrowsOnDisconnect) {
542 // Without a subscriber, the default in-flight limit applies.
543 uint32_t next = 0;
544 while (toClient->tryWrite(msg(next)))
545 ++next;
546 std::promise<bool> threw;
547 std::thread writer([&] {
548 try {
549 toClient->write(msg(next));
550 threw.set_value(false);
551 } catch (const std::exception &) {
552 threw.set_value(true);
553 }
554 });
555 std::this_thread::sleep_for(50ms);
556 toClient->disconnect();
557 auto f = threw.get_future();
558 ASSERT_EQ(f.wait_for(10s), std::future_status::ready);
559 EXPECT_TRUE(f.get());
560 writer.join();
561}
562
563} // namespace
static std::unique_ptr< RpcServer > server
static constexpr unsigned DefaultToClientMaxInFlight
Default maximum number of frames in flight (queued in the simulator or sent but not yet consumed by t...
Definition RpcClient.h:71
A concrete flat message backed by a single vector of bytes.
Definition Common.h:197
const T * as() const
Cast to a type.
Definition Common.h:232
static MessageData from(T &t)
Cast from a type to its raw bytes.
Definition Common.h:242
virtual void connect(ReadCallback callback, const ConnectOptions &options={})
Definition Ports.cpp:140
virtual std::future< MessageData > readAsync()
Asynchronous polling read.
Definition Ports.cpp:225
virtual void disconnect() override
Disconnect the channel.
Definition Ports.cpp:70
virtual void disconnect() override
Definition Ports.h:322
void write(const MessageData &data)
A very basic blocking write API.
Definition Ports.h:327
bool tryWrite(const MessageData &data)
A basic non-blocking write API.
Definition Ports.h:358
A client for the cosim RPC server.
Definition RpcClient.h:34
TODO: make this a proper backend (as much as possible).
Definition RpcServer.h:26
Definition esi.py:1