/** * @file TCPSClientGTest.cpp * @brief GTest coverage for TCPSClient (UDPS framing over TCP, consumer side). * * Each test runs the real TCPSClient either against the real TCPSServer or * against a scripted raw-socket "server" that misbehaves on purpose (bad * magic, oversize length, byte-at-a-time delivery, silence). * * @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and * the Development of Fusion Energy ('Fusion for Energy'). * Licensed under the EUPL, Version 1.1 or - as soon they will be approved * by the European Commission - subsequent versions of the EUPL (the "Licence") * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl */ #define DLL_API #include "gtest/gtest.h" #include #include #include "ConfigurationDatabase.h" #include "Sleep.h" #include "TCPSClient.h" #include "TCPSServer.h" #include "TCPTestUtil.h" using namespace MARTe; using namespace tcptest; namespace { /** Thread-safe recorder of everything the client delivers. */ class Recorder : public UDPSClientListener { public: Recorder() : connects(0u), disconnects(0u) { pthread_mutex_init(&mu, NULL); } ~Recorder() { pthread_mutex_destroy(&mu); } virtual void OnUDPSConfig(const uint8 *p, uint32 n) { pthread_mutex_lock(&mu); configs.push_back(std::vector(p, p + n)); order.push_back('C'); pthread_mutex_unlock(&mu); } virtual void OnUDPSData(const uint8 *p, uint32 n) { pthread_mutex_lock(&mu); datas.push_back(std::vector(p, p + n)); order.push_back('D'); pthread_mutex_unlock(&mu); } virtual void OnUDPSFragment(uint32 counter, uint32 nBytes, bool complete) { pthread_mutex_lock(&mu); counters.push_back(counter); pthread_mutex_unlock(&mu); } virtual void OnUDPSConnected() { pthread_mutex_lock(&mu); connects++; pthread_mutex_unlock(&mu); } virtual void OnUDPSDisconnected() { pthread_mutex_lock(&mu); disconnects++; pthread_mutex_unlock(&mu); } uint32 NumConfigs() { return Locked(configs.size()); } uint32 NumDatas() { return Locked(datas.size()); } uint32 Connects() { return Locked(connects); } uint32 Disconnects() { return Locked(disconnects); } std::vector Config(size_t i) { pthread_mutex_lock(&mu); std::vector v = configs[i]; pthread_mutex_unlock(&mu); return v; } std::vector Data(size_t i) { pthread_mutex_lock(&mu); std::vector v = datas[i]; pthread_mutex_unlock(&mu); return v; } uint32 Counter(size_t i) { pthread_mutex_lock(&mu); uint32 v = counters[i]; pthread_mutex_unlock(&mu); return v; } std::string Order() { pthread_mutex_lock(&mu); std::string v = order; pthread_mutex_unlock(&mu); return v; } private: uint32 Locked(size_t v) { pthread_mutex_lock(&mu); uint32 r = static_cast(v); pthread_mutex_unlock(&mu); return r; } pthread_mutex_t mu; std::vector > configs; std::vector > datas; std::vector counters; std::string order; uint32 connects; uint32 disconnects; }; bool InitClient(TCPSClient &c, uint16 port, float32 reconnect = 0.1f, float32 silence = 0.0f) { ConfigurationDatabase cdb; (void)cdb.Write("ServerAddr", "127.0.0.1"); (void)cdb.Write("Port", static_cast(port)); (void)cdb.Write("ReconnectDelay", reconnect); (void)cdb.Write("SilenceTimeout", silence); return c.Initialise(cdb); } bool InitServer(TCPSServer &s, uint16 port) { ConfigurationDatabase cdb; (void)cdb.Write("Port", static_cast(port)); return s.Initialise(cdb); } /** Pump the server until @p cond is true or timeout. */ #define PUMP_UNTIL(server, cond, timeoutMs, result) \ { \ const uint64 pumpDeadline = NowMs() + (timeoutMs); \ (result) = false; \ while (NowMs() < pumpDeadline) { \ (server).ServiceClients(); \ if (cond) { \ (result) = true; \ break; \ } \ Sleep::MSec(5u); \ } \ } #define WAIT_UNTIL(cond, timeoutMs, result) \ { \ const uint64 waitDeadline = NowMs() + (timeoutMs); \ (result) = false; \ while (NowMs() < waitDeadline) { \ if (cond) { \ (result) = true; \ break; \ } \ Sleep::MSec(5u); \ } \ } } // namespace TEST(TCPSClientGTest, TestInitialise_RequiresPort) { TCPSClient c; ConfigurationDatabase cdb; EXPECT_FALSE(c.Initialise(cdb)); } TEST(TCPSClientGTest, TestInitialise_RejectsNegativeTimes) { { TCPSClient c; ConfigurationDatabase cdb; (void)cdb.Write("Port", static_cast(44500u)); (void)cdb.Write("SilenceTimeout", -1.0f); EXPECT_FALSE(c.Initialise(cdb)); } { TCPSClient c; ConfigurationDatabase cdb; (void)cdb.Write("Port", static_cast(44500u)); (void)cdb.Write("ReconnectDelay", -1.0f); EXPECT_FALSE(c.Initialise(cdb)); } } TEST(TCPSClientGTest, TestReceivesConfigThenData) { TCPSServer server; ASSERT_TRUE(InitServer(server, 0u)); ASSERT_TRUE(server.Start()); std::vector cfg = Pattern(136u * 2u + 16u, 4u); ASSERT_TRUE(server.SendConfig(&cfg[0], static_cast(cfg.size()))); Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, server.GetPort())); client.SetListener(&rec); ASSERT_TRUE(client.Start()); bool ok; PUMP_UNTIL(server, rec.NumConfigs() == 1u, 3000u, ok); ASSERT_TRUE(ok); EXPECT_TRUE(client.IsConnected()); EXPECT_EQ(1u, rec.Connects()); EXPECT_EQ(cfg, rec.Config(0u)); for (uint32 c = 1u; c <= 20u; c++) { std::vector d = Pattern(300u, static_cast(c)); ASSERT_TRUE(server.SendData(c, &d[0], 300u)); } PUMP_UNTIL(server, rec.NumDatas() == 20u, 3000u, ok); ASSERT_TRUE(ok); for (uint32 c = 1u; c <= 20u; c++) { EXPECT_EQ(Pattern(300u, static_cast(c)), rec.Data(c - 1u)); EXPECT_EQ(c, rec.Counter(c - 1u)); } EXPECT_EQ("C" + std::string(20, 'D'), rec.Order()); EXPECT_EQ(0u, client.GetProtocolErrors()); EXPECT_TRUE(client.Stop()); EXPECT_EQ(1u, rec.Disconnects()); server.Stop(); } TEST(TCPSClientGTest, TestLargeFrame) { TCPSServer server; ASSERT_TRUE(InitServer(server, 0u)); ASSERT_TRUE(server.Start()); Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, server.GetPort())); client.SetListener(&rec); ASSERT_TRUE(client.Start()); bool ok; PUMP_UNTIL(server, server.GetClientCount() == 1u, 3000u, ok); ASSERT_TRUE(ok); std::vector big = Pattern(900000u, 11u); ASSERT_TRUE(server.SendData(1u, &big[0], static_cast(big.size()))); PUMP_UNTIL(server, rec.NumDatas() == 1u, 5000u, ok); ASSERT_TRUE(ok); EXPECT_TRUE(big == rec.Data(0u)); client.Stop(); server.Stop(); } TEST(TCPSClientGTest, TestReconnectAfterServerRestart) { uint16 port = 0u; int probe = Listen(port); ASSERT_GE(probe, 0); close(probe); TCPSServer server; ASSERT_TRUE(InitServer(server, port)); ASSERT_TRUE(server.Start()); std::vector cfg = Pattern(40u, 2u); ASSERT_TRUE(server.SendConfig(&cfg[0], 40u)); Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, port, 0.2f)); client.SetListener(&rec); ASSERT_TRUE(client.Start()); bool ok; PUMP_UNTIL(server, rec.NumConfigs() == 1u, 3000u, ok); ASSERT_TRUE(ok); server.Stop(); WAIT_UNTIL(!client.IsConnected(), 3000u, ok); EXPECT_TRUE(ok); EXPECT_EQ(1u, rec.Disconnects()); std::vector cfg2 = Pattern(60u, 9u); ASSERT_TRUE(server.Start()); ASSERT_TRUE(server.SendConfig(&cfg2[0], 60u)); PUMP_UNTIL(server, rec.NumConfigs() == 2u, 4000u, ok); ASSERT_TRUE(ok); EXPECT_EQ(cfg2, rec.Config(1u)); EXPECT_EQ(2u, client.GetConnectionCount()); EXPECT_EQ(2u, rec.Connects()); client.Stop(); server.Stop(); } TEST(TCPSClientGTest, TestRejectsBadMagic_ThenRecovers) { uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, port, 0.1f)); client.SetListener(&rec); ASSERT_TRUE(client.Start()); int s1 = AcceptOne(lfd, 3000); ASSERT_GE(s1, 0); std::vector junk(UDPS_HEADER_SIZE, 0xAAu); ASSERT_TRUE(SendAll(s1, &junk[0], junk.size())); bool ok; WAIT_UNTIL(client.GetProtocolErrors() == 1u, 3000u, ok); EXPECT_TRUE(ok); EXPECT_TRUE(PeerClosed(s1, 1000)); close(s1); // The client re-opens the connection and a well-formed stream works. int s2 = AcceptOne(lfd, 3000); ASSERT_GE(s2, 0); std::vector cfg = Pattern(20u, 1u); std::vector f = MakeFrame(UDPS_TYPE_CONFIG, 1u, cfg); ASSERT_TRUE(SendAll(s2, &f[0], f.size())); WAIT_UNTIL(rec.NumConfigs() == 1u, 3000u, ok); EXPECT_TRUE(ok); EXPECT_EQ(0u, rec.NumDatas()); client.Stop(); close(s2); close(lfd); } TEST(TCPSClientGTest, TestRejectsOversizeLength) { uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, port, 5.0f)); // no quick reconnect client.SetListener(&rec); ASSERT_TRUE(client.Start()); int s = AcceptOne(lfd, 3000); ASSERT_GE(s, 0); uint8 hdr[UDPS_HEADER_SIZE]; UDPSBuildHeader(hdr, UDPS_TYPE_DATA, 1u, 0u, 1u, UDPSClient::UDPS_CLIENT_MAX_PACKET_BYTES + 1u); ASSERT_TRUE(SendAll(s, hdr, sizeof(hdr))); bool ok; WAIT_UNTIL(client.GetProtocolErrors() == 1u, 3000u, ok); EXPECT_TRUE(ok); EXPECT_FALSE(client.IsConnected()); EXPECT_EQ(0u, rec.NumDatas()); client.Stop(); close(s); close(lfd); } TEST(TCPSClientGTest, TestRejectsFragmentedFrame) { uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, port, 5.0f)); client.SetListener(&rec); ASSERT_TRUE(client.Start()); int s = AcceptOne(lfd, 3000); ASSERT_GE(s, 0); std::vector f = MakeFrame(UDPS_TYPE_DATA, 1u, Pattern(10u, 1u), 2u); ASSERT_TRUE(SendAll(s, &f[0], f.size())); bool ok; WAIT_UNTIL(client.GetProtocolErrors() == 1u, 3000u, ok); EXPECT_TRUE(ok); EXPECT_EQ(0u, rec.NumDatas()); client.Stop(); close(s); close(lfd); } TEST(TCPSClientGTest, TestPartialFrameReads) { uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, port, 0.1f)); client.SetListener(&rec); ASSERT_TRUE(client.Start()); int s = AcceptOne(lfd, 3000); ASSERT_GE(s, 0); std::vector p1 = Pattern(64u, 1u); std::vector p2 = Pattern(3000u, 2u); std::vector f1 = MakeFrame(UDPS_TYPE_CONFIG, 1u, p1); std::vector f2 = MakeFrame(UDPS_TYPE_DATA, 5u, p2); // Frame 1: one byte at a time, so header and payload both split. for (size_t i = 0u; i < f1.size(); i++) { ASSERT_TRUE(SendAll(s, &f1[i], 1u)); if ((i % 8u) == 0u) { Sleep::MSec(2u); } } // Frame 2: split in three uneven chunks with pauses. size_t cuts[3] = {5u, 20u, f2.size() - 25u}; size_t at = 0u; for (int k = 0; k < 3; k++) { ASSERT_TRUE(SendAll(s, &f2[at], cuts[k] - at)); at = cuts[k]; Sleep::MSec(80u); } ASSERT_TRUE(SendAll(s, &f2[at], f2.size() - at)); // Frames 3+4 coalesced in one write. std::vector f3 = MakeFrame(UDPS_TYPE_DATA, 6u, Pattern(10u, 3u)); std::vector f4 = MakeFrame(UDPS_TYPE_DATA, 7u, Pattern(0u, 0u)); std::vector both(f3); both.insert(both.end(), f4.begin(), f4.end()); ASSERT_TRUE(SendAll(s, &both[0], both.size())); bool ok; WAIT_UNTIL(rec.NumDatas() == 3u, 3000u, ok); ASSERT_TRUE(ok); EXPECT_EQ(p1, rec.Config(0u)); EXPECT_EQ(p2, rec.Data(0u)); EXPECT_EQ(Pattern(10u, 3u), rec.Data(1u)); EXPECT_EQ(0u, rec.Data(2u).size()); EXPECT_EQ(5u, rec.Counter(0u)); EXPECT_EQ(7u, rec.Counter(2u)); EXPECT_EQ(0u, client.GetProtocolErrors()); client.Stop(); close(s); close(lfd); } TEST(TCPSClientGTest, TestUnknownFrameTypeIgnored) { uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, port)); client.SetListener(&rec); ASSERT_TRUE(client.Start()); int s = AcceptOne(lfd, 3000); ASSERT_GE(s, 0); std::vector a = MakeFrame(UDPS_TYPE_ACK, 1u, Pattern(4u, 1u)); std::vector b = MakeFrame(UDPS_TYPE_DATA, 2u, Pattern(4u, 2u)); ASSERT_TRUE(SendAll(s, &a[0], a.size())); ASSERT_TRUE(SendAll(s, &b[0], b.size())); bool ok; WAIT_UNTIL(rec.NumDatas() == 1u, 3000u, ok); EXPECT_TRUE(ok); EXPECT_EQ(0u, client.GetProtocolErrors()); client.Stop(); close(s); close(lfd); } TEST(TCPSClientGTest, TestSilenceTimeoutReconnects) { uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, port, 0.1f, 0.3f)); client.SetListener(&rec); ASSERT_TRUE(client.Start()); // The peer never sends anything: the client must give up and retry. bool ok; WAIT_UNTIL(client.GetConnectionCount() >= 2u, 4000u, ok); EXPECT_TRUE(ok); EXPECT_GE(rec.Disconnects(), 1u); client.Stop(); close(lfd); } TEST(TCPSClientGTest, TestNoServer_RetriesAndStopsPromptly) { uint16 port = 0u; int probe = Listen(port); ASSERT_GE(probe, 0); close(probe); // nothing listens on `port` now Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, port, 0.05f)); client.SetListener(&rec); ASSERT_TRUE(client.Start()); Sleep::MSec(300u); EXPECT_FALSE(client.IsConnected()); EXPECT_EQ(0u, client.GetConnectionCount()); const uint64 t0 = NowMs(); EXPECT_TRUE(client.Stop()); EXPECT_LT(NowMs() - t0, 1000u); // A server appearing later is picked up on restart. TCPSServer server; ASSERT_TRUE(InitServer(server, port)); ASSERT_TRUE(server.Start()); ASSERT_TRUE(client.Start()); bool ok; PUMP_UNTIL(server, client.IsConnected(), 3000u, ok); EXPECT_TRUE(ok); client.Stop(); server.Stop(); } TEST(TCPSClientGTest, TestStopWhileConnected_IsPrompt) { TCPSServer server; ASSERT_TRUE(InitServer(server, 0u)); ASSERT_TRUE(server.Start()); Recorder rec; TCPSClient client; ASSERT_TRUE(InitClient(client, server.GetPort())); client.SetListener(&rec); ASSERT_TRUE(client.Start()); bool ok; PUMP_UNTIL(server, client.IsConnected(), 3000u, ok); ASSERT_TRUE(ok); const uint64 t0 = NowMs(); EXPECT_TRUE(client.Stop()); EXPECT_LT(NowMs() - t0, 1000u); EXPECT_FALSE(client.IsConnected()); PUMP_UNTIL(server, server.GetClientCount() == 0u, 2000u, ok); EXPECT_TRUE(ok); server.Stop(); } // --------------------------------------------------------------------------- // Multiplexed connections (StreamName = "a,b,c") // --------------------------------------------------------------------------- namespace { bool InitMuxClient(TCPSClient &c, uint16 port, const char *names) { ConfigurationDatabase cdb; (void)cdb.Write("ServerAddr", "127.0.0.1"); (void)cdb.Write("Port", static_cast(port)); (void)cdb.Write("ReconnectDelay", 5.0f); (void)cdb.Write("StreamName", names); return c.Initialise(cdb); } /** Records the per-stream gap reported while each DATA frame is delivered. */ class GapRecorder : public Recorder { public: GapRecorder() : source(NULL_PTR(UDPSClientI *)) {} void Watch(UDPSClientI *s) { source = s; } virtual void OnUDPSData(const uint8 *p, uint32 n) { gaps.push_back(source != NULL_PTR(UDPSClientI *) ? source->GetLastDataGap() : 0xFFFFu); Recorder::OnUDPSData(p, n); } std::vector gaps; // read only after NumDatas() reached its target private: UDPSClientI *source; }; } // namespace TEST(TCPSClientGTest, TestMux_StreamNameValidation) { const char *const bad[] = { "a,,b", ",a", "a,", "a,b,a" }; for (size_t i = 0u; i < sizeof(bad) / sizeof(bad[0]); i++) { TCPSClient c; EXPECT_FALSE(InitMuxClient(c, 44500u, bad[i])) << bad[i]; } { // 65 distinct names exceed the 64 stream limit. std::string many; for (int i = 0; i < 65; i++) { char b[16]; snprintf(b, sizeof(b), "%ss%d", i ? "," : "", i); many += b; } TCPSClient c; EXPECT_FALSE(InitMuxClient(c, 44500u, many.c_str())); } { TCPSClient c; ASSERT_TRUE(InitMuxClient(c, 44500u, "fast,slow,third")); EXPECT_EQ(3u, c.GetNumStreams()); EXPECT_TRUE(c.GetStream(0u) != NULL_PTR(UDPSClientI *)); EXPECT_TRUE(c.GetStream(2u) != NULL_PTR(UDPSClientI *)); EXPECT_TRUE(c.GetStream(3u) == NULL_PTR(UDPSClientI *)); } { TCPSClient c; ASSERT_TRUE(InitMuxClient(c, 44500u, "only")); EXPECT_EQ(1u, c.GetNumStreams()); } } TEST(TCPSClientGTest, TestMux_SendsListAndDemuxesByIndex) { uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); GapRecorder r0, r1; TCPSClient client; ASSERT_TRUE(InitMuxClient(client, port, "fast,slow")); r0.Watch(client.GetStream(0u)); r1.Watch(client.GetStream(1u)); client.GetStream(0u)->SetListener(&r0); client.GetStream(1u)->SetListener(&r1); ASSERT_TRUE(client.GetStream(0u)->Start()); int s = AcceptOne(lfd, 3000); ASSERT_GE(s, 0); Frame sub; ASSERT_TRUE(ReadFrame(s, sub, 3000)); EXPECT_EQ(UDPS_TYPE_SUBSCRIBE, sub.type); EXPECT_EQ(std::string("fast,slow"), std::string(sub.payload.begin(), sub.payload.end())); std::vector cfg0 = Pattern(20u, 1u); std::vector cfg1 = Pattern(30u, 2u); std::vector f; f = MakeFrame(UDPS_TYPE_CONFIG, 1u, cfg0, 1u, 0u); ASSERT_TRUE(SendAll(s, &f[0], f.size())); f = MakeFrame(UDPS_TYPE_CONFIG, 1u, cfg1, 1u, 1u); ASSERT_TRUE(SendAll(s, &f[0], f.size())); // Stream 0: counters 1,2,3 ; stream 1: counters 10, 13 (gap of 2). const uint32 c0[] = { 1u, 2u, 3u }; for (size_t i = 0u; i < 3u; i++) { f = MakeFrame(UDPS_TYPE_DATA, c0[i], Pattern(8u, static_cast(c0[i])), 1u, 0u); ASSERT_TRUE(SendAll(s, &f[0], f.size())); } const uint32 c1[] = { 10u, 13u }; for (size_t i = 0u; i < 2u; i++) { f = MakeFrame(UDPS_TYPE_DATA, c1[i], Pattern(12u, static_cast(c1[i])), 1u, 1u); ASSERT_TRUE(SendAll(s, &f[0], f.size())); } bool ok; WAIT_UNTIL(r0.NumDatas() == 3u && r1.NumDatas() == 2u, 3000u, ok); ASSERT_TRUE(ok); EXPECT_EQ(cfg0, r0.Config(0u)); EXPECT_EQ(cfg1, r1.Config(0u)); EXPECT_EQ(1u, r0.NumConfigs()); EXPECT_EQ(1u, r1.NumConfigs()); EXPECT_EQ(Pattern(8u, 3u), r0.Data(2u)); EXPECT_EQ(Pattern(12u, 13u), r1.Data(1u)); ASSERT_EQ(3u, r0.gaps.size()); ASSERT_EQ(2u, r1.gaps.size()); EXPECT_EQ(0u, r0.gaps[0]); EXPECT_EQ(0u, r0.gaps[2]); EXPECT_EQ(0u, r1.gaps[0]); EXPECT_EQ(2u, r1.gaps[1]) << "gaps are tracked per stream"; EXPECT_EQ(0u, client.GetProtocolErrors()); client.GetStream(0u)->Stop(); close(s); close(lfd); } TEST(TCPSClientGTest, TestMux_StreamIndexOutOfRangeIsProtocolError) { uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); Recorder rec; TCPSClient client; ASSERT_TRUE(InitMuxClient(client, port, "a,b")); client.GetStream(0u)->SetListener(&rec); ASSERT_TRUE(client.GetStream(0u)->Start()); int s = AcceptOne(lfd, 3000); ASSERT_GE(s, 0); std::vector f = MakeFrame(UDPS_TYPE_DATA, 1u, Pattern(8u, 1u), 1u, 2u); ASSERT_TRUE(SendAll(s, &f[0], f.size())); bool ok; WAIT_UNTIL(client.GetProtocolErrors() == 1u, 3000u, ok); EXPECT_TRUE(ok); EXPECT_EQ(0u, rec.NumDatas()); client.GetStream(0u)->Stop(); close(s); close(lfd); } TEST(TCPSClientGTest, TestMux_FacadeStartStopIsReferenceCounted) { uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); TCPSClient client; ASSERT_TRUE(InitMuxClient(client, port, "a,b")); UDPSClientI *a = client.GetStream(0u); UDPSClientI *b = client.GetStream(1u); ASSERT_TRUE(a->Start()); ASSERT_TRUE(a->Start()); // idempotent per facade ASSERT_TRUE(b->Start()); int s = AcceptOne(lfd, 3000); ASSERT_GE(s, 0); bool ok; WAIT_UNTIL(client.IsConnected(), 3000u, ok); EXPECT_TRUE(ok); EXPECT_TRUE(a->Stop()); EXPECT_TRUE(a->Stop()); Sleep::MSec(150u); EXPECT_TRUE(client.IsConnected()) << "b still wants the connection"; EXPECT_TRUE(b->Stop()); WAIT_UNTIL(!client.IsConnected(), 3000u, ok); EXPECT_TRUE(ok); Frame sub; // drain the SUBSCRIBE the client sent on connect EXPECT_TRUE(ReadFrame(s, sub, 1000)); EXPECT_TRUE(PeerClosed(s, 1000)); close(s); close(lfd); }