/** * @file UDPSClientGTest.cpp * @brief GTest coverage for UDPSClient unicast keepalive. * * UDPSServer evicts silent unicast clients after its ClientTimeout (default * 30 s). UDPSClient must therefore re-send a keepalive ACK from the same * socket on KeepAliveInterval so the server refreshes its last-seen without * re-sending CONFIG. These tests drive a real UDPSClient against a local * UDP socket acting as the server and assert the wire behaviour; a final * pair runs the REAL UDPSServer + UDPSClient past the eviction deadline to * lock the fix in against regression. * * @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 * * @warning Unless required by applicable law or agreed to in writing, * software distributed under the Licence is distributed on an "AS IS" * basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the Licence permissions and limitations under the Licence. */ #define DLL_API /*---------------------------------------------------------------------------*/ /* Standard header includes */ /*---------------------------------------------------------------------------*/ #include "gtest/gtest.h" #include #include #include #include #include /*---------------------------------------------------------------------------*/ /* Project header includes */ /*---------------------------------------------------------------------------*/ #include "BasicUDPSocket.h" #include "ConfigurationDatabase.h" #include "FastPollingMutexSem.h" #include "InternetHost.h" #include "Sleep.h" #include "UDPSClient.h" #include "UDPSProtocol.h" #include "UDPSServer.h" using namespace MARTe; namespace { /** @return the bound local port of @p sock, or 0 on failure. */ uint16 GetBoundPort(BasicUDPSocket &sock) { struct sockaddr_in addr; socklen_t len = sizeof(addr); if (getsockname(sock.GetReadHandle(), reinterpret_cast(&addr), &len) != 0) { return 0u; } return ntohs(addr.sin_port); } /** * @brief Read one datagram from @p sock within @p timeoutMs. * @return true and fills @p type/@p srcPort on a valid UDPS datagram; false * on timeout or malformed packet. */ bool WaitDatagram(BasicUDPSocket &sock, int timeoutMs, uint8 &type, uint16 &srcPort) { int fd = sock.GetReadHandle(); if (fd < 0) { return false; } fd_set rset; FD_ZERO(&rset); FD_SET(fd, &rset); struct timeval tv; tv.tv_sec = timeoutMs / 1000; tv.tv_usec = (timeoutMs % 1000) * 1000; int nready = select(fd + 1, &rset, NULL, NULL, &tv); if (nready <= 0) { return false; } uint8 buf[UDPS_HEADER_SIZE]; uint32 size = UDPS_HEADER_SIZE; if (!sock.Read(reinterpret_cast(buf), size)) { return false; } if (size < UDPS_HEADER_SIZE) { return false; } const UDPSPacketHeader *hdr = reinterpret_cast(buf); if (hdr->magic != UDPS_MAGIC) { return false; } type = hdr->type; InternetHost src = sock.GetSource(); srcPort = src.GetPort(); return true; } /** * @brief Pump the UDPSServer service loop for @p durationMs, like UDPStreamer * does from its background thread. */ void PumpServer(UDPSServer &server, uint32 durationMs) { uint32 elapsed = 0u; while (elapsed < durationMs) { server.ServiceClients(); Sleep::MSec(20u); elapsed += 20u; } } /** * @brief Pump the server loop until a client registers (or timeout). * @return true if at least one client connected. */ bool WaitForClient(UDPSServer &server, uint32 timeoutMs) { uint32 elapsed = 0u; while (elapsed < timeoutMs) { server.ServiceClients(); if (server.GetClientCount() > 0u) { return true; } Sleep::MSec(20u); elapsed += 20u; } return false; } /*---------------------------------------------------------------------------*/ /* Fragment-reassembly test harness */ /*---------------------------------------------------------------------------*/ /** Largest reassembled payload the recording listener keeps a copy of. */ const uint32 kMaxRecordedBytes = 8192u; /** How many reassembled payloads the recording listener keeps. */ const uint32 kMaxRecorded = 16u; /** * @brief Listener that records every reassembled DATA/CONFIG payload. * * Callbacks run on the UDPSClient receive thread; the test thread reads the * records after a settle sleep, so both sides take the same lock. */ class RecordingListener: public UDPSClientListener { public: RecordingListener() : dataCount(0u), configCount(0u) { mux.Create(); } virtual void OnUDPSData(const uint8 *payload, uint32 payloadSize) { Record(dataPayloads, dataSizes, dataCount, payload, payloadSize); } virtual void OnUDPSConfig(const uint8 *payload, uint32 payloadSize) { Record(configPayloads, configSizes, configCount, payload, payloadSize); } uint32 DataCount() { (void) mux.FastLock(); uint32 n = dataCount; mux.FastUnLock(); return n; } uint32 ConfigCount() { (void) mux.FastLock(); uint32 n = configCount; mux.FastUnLock(); return n; } /** @return true iff record @p idx matches @p expected byte for byte. */ bool DataMatches(uint32 idx, const uint8 *expected, uint32 expectedSize) { return Matches(dataPayloads, dataSizes, dataCount, idx, expected, expectedSize); } bool ConfigMatches(uint32 idx, const uint8 *expected, uint32 expectedSize) { return Matches(configPayloads, configSizes, configCount, idx, expected, expectedSize); } uint32 DataSize(uint32 idx) { (void) mux.FastLock(); uint32 n = (idx < dataCount) ? dataSizes[idx] : 0u; mux.FastUnLock(); return n; } private: void Record(uint8 (&dst)[kMaxRecorded][kMaxRecordedBytes], uint32 (&sizes)[kMaxRecorded], uint32 &count, const uint8 *payload, uint32 payloadSize) { (void) mux.FastLock(); if (count < kMaxRecorded) { sizes[count] = payloadSize; uint32 n = (payloadSize < kMaxRecordedBytes) ? payloadSize : kMaxRecordedBytes; memcpy(dst[count], payload, n); count++; } mux.FastUnLock(); } bool Matches(uint8 (&src)[kMaxRecorded][kMaxRecordedBytes], uint32 (&sizes)[kMaxRecorded], uint32 &count, uint32 idx, const uint8 *expected, uint32 expectedSize) { (void) mux.FastLock(); bool ok = (idx < count) && (sizes[idx] == expectedSize) && (expectedSize <= kMaxRecordedBytes) && (memcmp(src[idx], expected, expectedSize) == 0); mux.FastUnLock(); return ok; } FastPollingMutexSem mux; uint8 dataPayloads[kMaxRecorded][kMaxRecordedBytes]; uint32 dataSizes[kMaxRecorded]; uint32 dataCount; uint8 configPayloads[kMaxRecorded][kMaxRecordedBytes]; uint32 configSizes[kMaxRecorded]; uint32 configCount; }; /** Fill @p buf with a position-dependent pattern so misplacement is visible. */ void FillPattern(uint8 *buf, uint32 n, uint8 seed) { for (uint32 i = 0u; i < n; i++) { buf[i] = static_cast((i * 7u) + seed); } } /** Send one UDPS fragment datagram to 127.0.0.1:@p dstPort. */ bool SendFragment(BasicUDPSocket &sock, uint16 dstPort, uint8 type, uint32 counter, uint16 fragIdx, uint16 totalFrags, const uint8 *payload, uint32 payloadBytes) { uint8 buf[UDPS_HEADER_SIZE + 2048u]; if (payloadBytes > 2048u) { return false; } UDPSBuildHeader(buf, type, counter, fragIdx, totalFrags, payloadBytes); memcpy(&buf[UDPS_HEADER_SIZE], payload, payloadBytes); InternetHost dst(dstPort, "127.0.0.1"); (void) sock.SetDestination(dst); uint32 n = UDPS_HEADER_SIZE + payloadBytes; return sock.Write(reinterpret_cast(buf), n); } /** * @brief Bring up a UDPSClient pointed at @p server and learn the ephemeral * port it receives DATA on (the source port of its CONNECT). * * Silence timeout and keepalive are disabled so the session never churns * underneath the fragments the test injects. */ bool StartClientAndLearnPort(UDPSClient &client, ConfigurationDatabase &cfg, BasicUDPSocket &server, uint16 serverPort, uint16 &clientPort) { if (!cfg.Write("ServerAddr", "127.0.0.1")) { return false; } if (!cfg.Write("Port", static_cast(serverPort))) { return false; } if (!cfg.Write("SilenceTimeout", 0.0f)) { return false; } if (!cfg.Write("KeepAliveInterval", 0u)) { return false; } if (!client.Initialise(cfg)) { return false; } if (!client.Start()) { return false; } uint8 type = 0xFFu; if (!WaitDatagram(server, 3000, type, clientPort)) { return false; } return (type == UDPS_TYPE_CONNECT) && (clientPort != 0u); } } // namespace /*---------------------------------------------------------------------------*/ /* Method definitions */ /*---------------------------------------------------------------------------*/ TEST(UDPSClientGTest, TestUnicastKeepAliveSendsPeriodicAck) { /* Fake server socket (ephemeral port) */ BasicUDPSocket server; ASSERT_TRUE(server.Open()); ASSERT_TRUE(server.Listen(0u)); uint16 serverPort = GetBoundPort(server); ASSERT_NE(serverPort, 0u); ConfigurationDatabase cfg; ASSERT_TRUE(cfg.Write("ServerAddr", "127.0.0.1")); ASSERT_TRUE(cfg.Write("Port", static_cast(serverPort))); ASSERT_TRUE(cfg.Write("KeepAliveInterval", 1u)); /* SilenceTimeout=0 keeps the session stable for the whole test */ ASSERT_TRUE(cfg.Write("SilenceTimeout", 0.0f)); UDPSClient client; ASSERT_TRUE(client.Initialise(cfg)); ASSERT_TRUE(client.Start()); /* 1) CONNECT from the client's ephemeral socket */ uint8 type = 0xFFu; uint16 clientPort = 0u; ASSERT_TRUE(WaitDatagram(server, 3000, type, clientPort)); EXPECT_EQ(type, UDPS_TYPE_CONNECT); ASSERT_NE(clientPort, 0u); /* 2) Keepalive ACKs arrive periodically from the SAME socket */ uint32 acks = 0u; uint32 elapsedMs = 0u; while ((acks < 2u) && (elapsedMs < 3500u)) { uint8 t = 0xFFu; uint16 port = 0u; bool got = WaitDatagram(server, 1000, t, port); elapsedMs += 1000u; if (!got) { continue; } if ((t == UDPS_TYPE_ACK) && (port == clientPort)) { acks++; } } EXPECT_GE(acks, 2u); client.Stop(); server.Close(); } TEST(UDPSClientGTest, TestKeepAliveDisabledWhenIntervalZero) { BasicUDPSocket server; ASSERT_TRUE(server.Open()); ASSERT_TRUE(server.Listen(0u)); uint16 serverPort = GetBoundPort(server); ASSERT_NE(serverPort, 0u); ConfigurationDatabase cfg; ASSERT_TRUE(cfg.Write("ServerAddr", "127.0.0.1")); ASSERT_TRUE(cfg.Write("Port", static_cast(serverPort))); ASSERT_TRUE(cfg.Write("KeepAliveInterval", 0u)); ASSERT_TRUE(cfg.Write("SilenceTimeout", 0.0f)); UDPSClient client; ASSERT_TRUE(client.Initialise(cfg)); ASSERT_TRUE(client.Start()); uint8 type = 0xFFu; uint16 clientPort = 0u; ASSERT_TRUE(WaitDatagram(server, 3000, type, clientPort)); EXPECT_EQ(type, UDPS_TYPE_CONNECT); /* No keepalive configured: nothing else must arrive */ EXPECT_FALSE(WaitDatagram(server, 2000, type, clientPort)); client.Stop(); server.Close(); } TEST(UDPSClientGTest, TestKeepAlivePreventsServerEviction) { /* Regression test for the 30 s unicast disconnect: UDPSServer evicts a * silent client after ClientTimeout; the client's periodic ACKs must * keep it registered. Drive the REAL server + client pair, like * UDPStreamer + StreamHub do, past the eviction deadline. */ /* Free-port probe (UDP has no TIME_WAIT) */ BasicUDPSocket probe; ASSERT_TRUE(probe.Open()); ASSERT_TRUE(probe.Listen(0u)); uint16 serverPort = GetBoundPort(probe); probe.Close(); ASSERT_NE(serverPort, 0u); /* Server with a short eviction timeout so the test is fast */ ConfigurationDatabase serverCfg; ASSERT_TRUE(serverCfg.Write("Port", static_cast(serverPort))); ASSERT_TRUE(serverCfg.Write("ClientTimeout", 3u)); UDPSServer server; ASSERT_TRUE(server.Initialise(serverCfg)); ASSERT_TRUE(server.Start()); /* Client: keepalive every 1 s (< server timeout), silence disabled */ ConfigurationDatabase clientCfg; ASSERT_TRUE(clientCfg.Write("ServerAddr", "127.0.0.1")); ASSERT_TRUE(clientCfg.Write("Port", static_cast(serverPort))); ASSERT_TRUE(clientCfg.Write("KeepAliveInterval", 1u)); ASSERT_TRUE(clientCfg.Write("SilenceTimeout", 0.0f)); UDPSClient client; ASSERT_TRUE(client.Initialise(clientCfg)); ASSERT_TRUE(client.Start()); ASSERT_TRUE(WaitForClient(server, 3000)); /* CONNECT registered */ EXPECT_EQ(server.GetClientCount(), 1u); /* Pump well past ClientTimeout: keepalive ACKs must prevent eviction */ PumpServer(server, 5000); EXPECT_EQ(server.GetClientCount(), 1u); client.Stop(); server.Stop(); } TEST(UDPSClientGTest, TestServerEvictsWithoutKeepAlive) { /* Negative control: without keepalive the same harness MUST evict, which * proves TestKeepAlivePreventsServerEviction passes because of the ACKs * and not because eviction is broken. */ BasicUDPSocket probe; ASSERT_TRUE(probe.Open()); ASSERT_TRUE(probe.Listen(0u)); uint16 serverPort = GetBoundPort(probe); probe.Close(); ASSERT_NE(serverPort, 0u); ConfigurationDatabase serverCfg; ASSERT_TRUE(serverCfg.Write("Port", static_cast(serverPort))); ASSERT_TRUE(serverCfg.Write("ClientTimeout", 3u)); UDPSServer server; ASSERT_TRUE(server.Initialise(serverCfg)); ASSERT_TRUE(server.Start()); ConfigurationDatabase clientCfg; ASSERT_TRUE(clientCfg.Write("ServerAddr", "127.0.0.1")); ASSERT_TRUE(clientCfg.Write("Port", static_cast(serverPort))); ASSERT_TRUE(clientCfg.Write("KeepAliveInterval", 0u)); ASSERT_TRUE(clientCfg.Write("SilenceTimeout", 0.0f)); UDPSClient client; ASSERT_TRUE(client.Initialise(clientCfg)); ASSERT_TRUE(client.Start()); ASSERT_TRUE(WaitForClient(server, 3000)); /* CONNECT registered */ EXPECT_EQ(server.GetClientCount(), 1u); /* No ACKs: the server must evict after ClientTimeout */ PumpServer(server, 5000); EXPECT_EQ(server.GetClientCount(), 0u); client.Stop(); server.Stop(); } TEST(UDPSClientGTest, TestSilenceTimeoutSubSecondTriggersReconnect) { /* SilenceTimeout is float32 seconds: a sub-second value must actually * fire (integer truncation would silently disable the check). */ BasicUDPSocket server; ASSERT_TRUE(server.Open()); ASSERT_TRUE(server.Listen(0u)); uint16 serverPort = GetBoundPort(server); ASSERT_NE(serverPort, 0u); ConfigurationDatabase cfg; ASSERT_TRUE(cfg.Write("ServerAddr", "127.0.0.1")); ASSERT_TRUE(cfg.Write("Port", static_cast(serverPort))); ASSERT_TRUE(cfg.Write("SilenceTimeout", 0.3f)); ASSERT_TRUE(cfg.Write("ReconnectDelay", 0u)); /* reconnect immediately */ ASSERT_TRUE(cfg.Write("KeepAliveInterval", 0u)); UDPSClient client; ASSERT_TRUE(client.Initialise(cfg)); ASSERT_TRUE(client.Start()); /* 1) First CONNECT from the client's ephemeral socket */ uint8 type = 0xFFu; uint16 portA = 0u; ASSERT_TRUE(WaitDatagram(server, 3000, type, portA)); EXPECT_EQ(type, UDPS_TYPE_CONNECT); ASSERT_NE(portA, 0u); /* 2) The server sends nothing: after ~0.3 s the client must disconnect * and re-announce with a NEW ephemeral socket. Fails if the timeout * was truncated to 0 (disabled) or left at the old 5 s default. */ uint32 elapsedMs = 0u; bool reconnected = false; while ((elapsedMs < 2000u) && !reconnected) { uint8 t = 0xFFu; uint16 p = 0u; bool got = WaitDatagram(server, 500, t, p); elapsedMs += 500u; if (!got) { continue; } /* DISCONNECT from the old socket is expected; only a CONNECT from a * new source port proves the reconnect happened. */ if ((t == UDPS_TYPE_CONNECT) && (p != portA)) { reconnected = true; } } EXPECT_TRUE(reconnected); client.Stop(); server.Close(); } TEST(UDPSClientGTest, TestReorderedFragmentsAreReassembled) { /* UDP gives no ordering guarantee: the fragments of one packet may arrive * in any order, with nothing lost. Reassembly must not depend on fragment * 0 arriving first — if it does, an out-of-order burst destroys a packet * whose bytes all arrived, and leaves a slot occupied until the 2 s GC, * which is how four slots end up permanently full. */ BasicUDPSocket server; ASSERT_TRUE(server.Open()); ASSERT_TRUE(server.Listen(0u)); uint16 serverPort = GetBoundPort(server); ASSERT_NE(serverPort, 0u); RecordingListener listener; UDPSClient client; client.SetListener(&listener); ConfigurationDatabase cfg; uint16 clientPort = 0u; ASSERT_TRUE(StartClientAndLearnPort(client, cfg, server, serverPort, clientPort)); /* 20-byte payload over three 8-byte chunks: the last one is short, which * is exactly why chunk size has to be learnt from a non-last fragment. */ uint8 expected[20]; FillPattern(expected, sizeof(expected), 3u); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 7u, 1u, 3u, &expected[8], 8u)); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 7u, 2u, 3u, &expected[16], 4u)); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 7u, 0u, 3u, &expected[0], 8u)); Sleep::MSec(400u); ASSERT_EQ(listener.DataCount(), 1u) << "no fragment was lost, yet the packet was not delivered"; EXPECT_EQ(listener.DataSize(0u), 20u); EXPECT_TRUE(listener.DataMatches(0u, expected, sizeof(expected))); client.Stop(); server.Close(); } TEST(UDPSClientGTest, TestDataAndConfigWithSameCounterDoNotCollide) { /* DATA and CONFIG carry independent counter sequences, so the same counter * value legitimately appears on both. A reassembly slot keyed on the * counter alone merges the two streams: one payload is delivered under the * wrong type and the other is silently dropped. */ BasicUDPSocket server; ASSERT_TRUE(server.Open()); ASSERT_TRUE(server.Listen(0u)); uint16 serverPort = GetBoundPort(server); ASSERT_NE(serverPort, 0u); RecordingListener listener; UDPSClient client; client.SetListener(&listener); ConfigurationDatabase cfg; uint16 clientPort = 0u; ASSERT_TRUE(StartClientAndLearnPort(client, cfg, server, serverPort, clientPort)); uint8 dataPayload[16]; uint8 cfgPayload[16]; FillPattern(dataPayload, sizeof(dataPayload), 11u); FillPattern(cfgPayload, sizeof(cfgPayload), 200u); /* Same counter (42), interleaved, two fragments each. */ ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_CONFIG, 42u, 0u, 2u, &cfgPayload[0], 8u)); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 42u, 0u, 2u, &dataPayload[0], 8u)); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_CONFIG, 42u, 1u, 2u, &cfgPayload[8], 8u)); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 42u, 1u, 2u, &dataPayload[8], 8u)); Sleep::MSec(400u); EXPECT_EQ(listener.ConfigCount(), 1u); EXPECT_TRUE(listener.ConfigMatches(0u, cfgPayload, sizeof(cfgPayload))); ASSERT_EQ(listener.DataCount(), 1u) << "the DATA packet was swallowed by the CONFIG slot sharing its counter"; EXPECT_TRUE(listener.DataMatches(0u, dataPayload, sizeof(dataPayload))); client.Stop(); server.Close(); } TEST(UDPSClientGTest, TestDuplicateHighIndexFragmentDoesNotFakeCompletion) { /* Completion is decided by counting fragments, with a received-bitmask to * reject duplicates. If the mask is narrower than the fragment count the * client accepts, a duplicated high-index fragment is counted twice and * the packet is delivered while a fragment is still missing — a payload * with a hole of stale bytes, reported as valid. */ BasicUDPSocket server; ASSERT_TRUE(server.Open()); ASSERT_TRUE(server.Listen(0u)); uint16 serverPort = GetBoundPort(server); ASSERT_NE(serverPort, 0u); RecordingListener listener; UDPSClient client; client.SetListener(&listener); ConfigurationDatabase cfg; uint16 clientPort = 0u; ASSERT_TRUE(StartClientAndLearnPort(client, cfg, server, serverPort, clientPort)); /* 300 fragments — past the 256 a 32-byte mask covers, but well inside the * 512 the client's own sanity check permits. */ const uint16 kTotalFrags = 300u; const uint32 kChunk = 8u; const uint32 kLastChunk = 4u; const uint32 kTotalBytes = ((kTotalFrags - 1u) * kChunk) + kLastChunk; uint8 expected[((kTotalFrags - 1u) * kChunk) + kLastChunk]; FillPattern(expected, kTotalBytes, 5u); /* Everything except the final fragment, plus one duplicate above 255. */ for (uint16 f = 0u; f < (kTotalFrags - 1u); f++) { ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 9u, f, kTotalFrags, &expected[f * kChunk], kChunk)); } ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 9u, 260u, kTotalFrags, &expected[260u * kChunk], kChunk)); Sleep::MSec(500u); ASSERT_EQ(listener.DataCount(), 0u) << "delivered with a fragment still missing (a duplicate was counted " "as a new fragment)"; /* The genuinely missing fragment completes it, with the right bytes. */ ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 9u, kTotalFrags - 1u, kTotalFrags, &expected[(kTotalFrags - 1u) * kChunk], kLastChunk)); Sleep::MSec(400u); ASSERT_EQ(listener.DataCount(), 1u); EXPECT_EQ(listener.DataSize(0u), kTotalBytes); EXPECT_TRUE(listener.DataMatches(0u, expected, kTotalBytes)); client.Stop(); server.Close(); } TEST(UDPSClientGTest, TestStaleDataPacketIsNotDelivered) { /* A DATA packet that arrives after a newer one has already been delivered * carries an older time base. Delivering it makes the consumer place its * samples behind the ones it has: they collide with what is already * plotted, and the range they should have occupied stays empty. The * counter is the only thing that tells the two apart, so the client must * drop anything that does not advance it. */ BasicUDPSocket server; ASSERT_TRUE(server.Open()); ASSERT_TRUE(server.Listen(0u)); uint16 serverPort = GetBoundPort(server); ASSERT_NE(serverPort, 0u); RecordingListener listener; UDPSClient client; client.SetListener(&listener); ConfigurationDatabase cfg; uint16 clientPort = 0u; ASSERT_TRUE(StartClientAndLearnPort(client, cfg, server, serverPort, clientPort)); uint8 pkt[8]; FillPattern(pkt, sizeof(pkt), 1u); /* 10 and 11 advance the counter; 9 and the repeat of 11 do not. */ ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 10u, 0u, 1u, pkt, sizeof(pkt))); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 11u, 0u, 1u, pkt, sizeof(pkt))); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 9u, 0u, 1u, pkt, sizeof(pkt))); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 11u, 0u, 1u, pkt, sizeof(pkt))); Sleep::MSec(400u); EXPECT_EQ(listener.DataCount(), 2u) << "a packet older than one already delivered reached the listener"; EXPECT_EQ(client.GetStaleDataPackets(), 2u); client.Stop(); server.Close(); } TEST(UDPSClientGTest, TestCounterGapIsReported) { /* Consumers that infer a sample period from the sender-clock gap need to * know how many packets that gap spans; without it a single loss reads as * a halved rate. The gap comes from the counter, and must exclude the * packet being delivered. */ BasicUDPSocket server; ASSERT_TRUE(server.Open()); ASSERT_TRUE(server.Listen(0u)); uint16 serverPort = GetBoundPort(server); ASSERT_NE(serverPort, 0u); RecordingListener listener; UDPSClient client; client.SetListener(&listener); ConfigurationDatabase cfg; uint16 clientPort = 0u; ASSERT_TRUE(StartClientAndLearnPort(client, cfg, server, serverPort, clientPort)); uint8 pkt[8]; FillPattern(pkt, sizeof(pkt), 2u); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 100u, 0u, 1u, pkt, sizeof(pkt))); Sleep::MSec(200u); EXPECT_EQ(client.GetLastDataGap(), 0u) << "the first packet lost nothing"; /* 101, 102 and 103 never arrive. */ ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 104u, 0u, 1u, pkt, sizeof(pkt))); Sleep::MSec(200u); EXPECT_EQ(client.GetLastDataGap(), 3u); ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, 105u, 0u, 1u, pkt, sizeof(pkt))); Sleep::MSec(200u); EXPECT_EQ(client.GetLastDataGap(), 0u) << "the gap must not persist"; EXPECT_EQ(listener.DataCount(), 3u); EXPECT_EQ(client.GetStaleDataPackets(), 0u); client.Stop(); server.Close(); } TEST(UDPSClientGTest, TestCounterWraparoundDoesNotRejectStream) { /* The counter is a uint32 that wraps. Ordering it by plain comparison * would call every packet after the wrap older than 0xFFFFFFFF and reject * the stream permanently, so the ordering has to be done on the signed * difference. */ BasicUDPSocket server; ASSERT_TRUE(server.Open()); ASSERT_TRUE(server.Listen(0u)); uint16 serverPort = GetBoundPort(server); ASSERT_NE(serverPort, 0u); RecordingListener listener; UDPSClient client; client.SetListener(&listener); ConfigurationDatabase cfg; uint16 clientPort = 0u; ASSERT_TRUE(StartClientAndLearnPort(client, cfg, server, serverPort, clientPort)); uint8 pkt[8]; FillPattern(pkt, sizeof(pkt), 4u); const uint32 counters[4] = { 0xFFFFFFFEu, 0xFFFFFFFFu, 0u, 1u }; for (uint32 i = 0u; i < 4u; i++) { ASSERT_TRUE(SendFragment(server, clientPort, UDPS_TYPE_DATA, counters[i], 0u, 1u, pkt, sizeof(pkt))); Sleep::MSec(150u); } EXPECT_EQ(listener.DataCount(), 4u) << "the stream was rejected across the counter wrap"; EXPECT_EQ(client.GetStaleDataPackets(), 0u); EXPECT_EQ(client.GetLastDataGap(), 0u); client.Stop(); server.Close(); }