/** * @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 "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; } } // 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(); }