/** * @file TCPStreamerGTest.cpp * @brief GTests for the TCPStreamer / TCPStreamerClient DataSources. * * Each test builds a RealTimeApplication from a configuration string (the same * pattern as the UDPStreamer tests). No scheduler thread is started: the tests * write the DataSource memory and call Synchronise() themselves. */ #define DLL_API #include #include #include #include "gtest/gtest.h" #include "AdvancedErrorManagement.h" #include "ClassRegistryDatabase.h" #include "ConfigurationDatabase.h" #include "GAM.h" #include "MemoryOperationsHelper.h" #include "ObjectRegistryDatabase.h" #include "RealTimeApplication.h" #include "Sleep.h" #include "StandardParser.h" #include "TCPStreamer.h" #include "TCPStreamerClient.h" #include "TCPTestUtil.h" #include "UDPSProtocol.h" using namespace MARTe; using namespace tcptest; namespace { /** Output-only GAM; its presence is what makes the streamer DataSource configure. */ class TCPStreamerTestWriterGAM : public GAM { public: CLASS_REGISTER_DECLARATION() TCPStreamerTestWriterGAM() : GAM() {} virtual ~TCPStreamerTestWriterGAM() {} virtual bool Setup() { return true; } virtual bool Execute() { return true; } }; CLASS_REGISTER(TCPStreamerTestWriterGAM, "1.0") /** Copies its input to its output; makes the client DataSource configure. */ class TCPStreamerTestReaderGAM : public GAM { public: CLASS_REGISTER_DECLARATION() TCPStreamerTestReaderGAM() : GAM() {} virtual ~TCPStreamerTestReaderGAM() {} virtual bool Setup() { return true; } virtual bool Execute() { void *in = GetInputSignalMemory(0u); void *out = GetOutputSignalMemory(0u); if ((in != NULL_PTR(void *)) && (out != NULL_PTR(void *))) { (void)MemoryOperationsHelper::Copy(out, in, sizeof(uint32)); } return true; } }; CLASS_REGISTER(TCPStreamerTestReaderGAM, "1.0") ReferenceT LoadApplication(const std::string &config) { ConfigurationDatabase cdb; StreamString cfgStr = config.c_str(); (void)cfgStr.Seek(0LLU); StandardParser parser(cfgStr, cdb); if (!parser.Parse()) { return ReferenceT(); } ObjectRegistryDatabase *god = ObjectRegistryDatabase::Instance(); god->Purge(); if (!god->Initialise(cdb)) { return ReferenceT(); } ReferenceT app = god->Find("Test"); if (!app.IsValid() || !app->ConfigureApplication()) { return ReferenceT(); } return app; } /** * Application with an optional streamer DS ("Streamer", fed by Writer) and an * optional client DS ("Client", read by Reader). @p streamerExtra / @p clientExtra * are spliced verbatim into the DataSource blocks. */ std::string BuildConfig(bool withStreamer, const std::string &streamerExtra, bool withClient, const std::string &clientExtra) { std::string cfg = "+Test = {\n Class = RealTimeApplication\n +Functions = {\n Class = ReferenceContainer\n"; std::string functions; if (withStreamer) { cfg += " +Writer = { Class = TCPStreamerTestWriterGAM\n" " OutputSignals = { Counter = { DataSource = Streamer Type = uint32 } } }\n"; functions += " Writer"; } if (withClient) { cfg += " +Reader = { Class = TCPStreamerTestReaderGAM\n" " InputSignals = { Counter = { DataSource = Client Type = uint32 } }\n" " OutputSignals = { Counter = { DataSource = DDB Type = uint32 } } }\n"; functions += " Reader"; } cfg += " }\n +Data = {\n Class = ReferenceContainer\n"; if (withClient) { /* A GAMDataSource with no signals fails to allocate, so only add it when used. */ cfg += " DefaultDataSource = DDB\n +DDB = { Class = GAMDataSource }\n"; } cfg += " +Timings = { Class = TimingDataSource }\n"; if (withStreamer) { cfg += " +Streamer = { Class = TCPStreamer\n" + streamerExtra + "\n Signals = { Counter = { Type = uint32 } } }\n"; } if (withClient) { cfg += " +Client = { Class = TCPStreamerClient\n" + clientExtra + "\n Signals = { Counter = { Type = uint32 } } }\n"; } cfg += " }\n +States = {\n Class = ReferenceContainer\n +State1 = { Class = RealTimeState\n" " +Threads = { Class = ReferenceContainer\n" " +Thread1 = { Class = RealTimeThread Functions = {" + functions + " } } } } }\n" " +Scheduler = { Class = GAMScheduler TimingDataSource = Timings }\n}\n"; return cfg; } std::string PortStr(uint16 port) { char buf[16]; snprintf(buf, sizeof(buf), "%u", static_cast(port)); return buf; } /** Closes the application on scope exit so a failing ASSERT cannot leak threads or ports. */ struct AppGuard { ~AppGuard() { ObjectRegistryDatabase::Instance()->Purge(); } }; void SetCounter(ReferenceT &ds, uint32 v) { void *mem = NULL_PTR(void *); ASSERT_TRUE(ds->GetSignalMemoryBuffer(0u, 0u, mem)); memcpy(mem, &v, sizeof(v)); } /** CONFIG payload for the single uint32 "Counter" signal, Strict publishing. */ std::vector MakeConfigPayload() { std::vector p(4u + UDPS_SIGNAL_DESC_SIZE + 1u, 0u); const uint32 numSigs = 1u; memcpy(&p[0], &numSigs, 4u); UDPSSignalDescriptor desc; memset(&desc, 0, sizeof(desc)); strncpy(desc.name, "Counter", UDPS_MAX_SIGNAL_NAME - 1u); desc.typeCode = UDPS_TYPECODE_UINT32; desc.quantType = UDPS_QUANT_NONE; desc.numDimensions = 0u; desc.numRows = 1u; desc.numCols = 1u; desc.rangeMax = 1.0; desc.timeMode = UDPS_TIMEMODE_PACKET; desc.timeSignalIdx = UDPS_NO_TIME_SIGNAL; memcpy(&p[4], &desc, UDPS_SIGNAL_DESC_SIZE); p[4u + UDPS_SIGNAL_DESC_SIZE] = UDPS_PUBLISH_STRICT; return p; } /** DATA payload: 8-byte timestamp then the uint32 value. */ std::vector MakeDataPayload(uint32 value) { std::vector p(12u, 0u); memcpy(&p[8], &value, 4u); return p; } } // namespace /*---------------------------------------------------------------------------*/ /* TCPStreamer */ /*---------------------------------------------------------------------------*/ TEST(TCPStreamerGTest, ClassesAreRegistered) { ClassRegistryDatabase *db = ClassRegistryDatabase::Instance(); EXPECT_TRUE(db->Find("TCPStreamer") != NULL_PTR(const ClassRegistryItem *)); EXPECT_TRUE(db->Find("TCPStreamerClient") != NULL_PTR(const ClassRegistryItem *)); } TEST(TCPStreamerGTest, StreamerDefaults) { AppGuard guard; ReferenceT app = LoadApplication(BuildConfig(true, "", false, "")); ASSERT_TRUE(app.IsValid()); ReferenceT ds = ObjectRegistryDatabase::Instance()->Find("Test.Data.Streamer"); ASSERT_TRUE(ds.IsValid()); EXPECT_EQ(44500u, ds->GetPort()); EXPECT_EQ(60000u, ds->GetMaxPayloadSize()); EXPECT_EQ(0u, ds->GetClientCount()); EXPECT_EQ(0u, ds->GetClientDrops()); EXPECT_FALSE(ds->IsClientConnected()); } TEST(TCPStreamerGTest, StreamerRejectsUdpOnlyKeys) { const char *const keys[] = { "MulticastGroup = \"239.0.0.1\"", "Interface = \"127.0.0.1\"", "DataPort = 44501", "ClientTimeout = 5" }; for (size_t i = 0u; i < sizeof(keys) / sizeof(keys[0]); i++) { AppGuard guard; ReferenceT app = LoadApplication(BuildConfig(true, std::string("Port = 44820\n") + keys[i], false, "")); EXPECT_FALSE(app.IsValid()) << keys[i]; } } TEST(TCPStreamerGTest, StreamerAcceptsAndValidatesTcpKeys) { { AppGuard guard; ReferenceT app = LoadApplication(BuildConfig( true, "Port = 44821 MaxClients = 4 ClientQueueDepth = 2 ClientStallTimeout = 0 SendBufferSize = 65536", false, "")); EXPECT_TRUE(app.IsValid()); } const char *const bad[] = { "MaxClients = 0", "ClientQueueDepth = 0", "MaxPayloadSize = 4" }; for (size_t i = 0u; i < sizeof(bad) / sizeof(bad[0]); i++) { AppGuard guard; ReferenceT app = LoadApplication(BuildConfig(true, std::string("Port = 44822 ") + bad[i], false, "")); EXPECT_FALSE(app.IsValid()) << bad[i]; } } TEST(TCPStreamerGTest, StreamerDeliversConfigThenData) { AppGuard guard; ReferenceT app = LoadApplication(BuildConfig(true, "Port = 44823", false, "")); ASSERT_TRUE(app.IsValid()); ReferenceT ds = ObjectRegistryDatabase::Instance()->Find("Test.Data.Streamer"); ASSERT_TRUE(ds.IsValid()); ASSERT_TRUE(app->PrepareNextState("State1") == ErrorManagement::NoError); EXPECT_EQ(44823u, ds->GetBoundPort()); int fd = Connect(44823u); ASSERT_GE(fd, 0); Frame f; ASSERT_TRUE(ReadFrame(fd, f, 3000)); EXPECT_EQ(UDPS_TYPE_CONFIG, f.type); EXPECT_EQ(1u, f.totalFragments); ASSERT_GE(f.payload.size(), MakeConfigPayload().size()); uint32 numSigs = 0u; memcpy(&numSigs, &f.payload[0], 4u); EXPECT_EQ(1u, numSigs); UDPSSignalDescriptor desc; memcpy(&desc, &f.payload[4], UDPS_SIGNAL_DESC_SIZE); EXPECT_STREQ("Counter", desc.name); EXPECT_EQ(UDPS_TYPECODE_UINT32, desc.typeCode); /* The client count updates on the sender thread's next service pass. */ for (int i = 0; (i < 100) && (ds->GetClientCount() == 0u); i++) { Sleep::MSec(10u); } EXPECT_EQ(1u, ds->GetClientCount()); EXPECT_TRUE(ds->IsClientConnected()); for (uint32 v = 100u; v < 105u; v++) { SetCounter(ds, v); ASSERT_TRUE(ds->Synchronise()); ASSERT_TRUE(ReadFrame(fd, f, 3000)); EXPECT_EQ(UDPS_TYPE_DATA, f.type); EXPECT_EQ(1u, f.totalFragments); ASSERT_EQ(12u, f.payload.size()); uint32 got = 0u; memcpy(&got, &f.payload[8], 4u); EXPECT_EQ(v, got); Sleep::MSec(5u); /* one publication per sender wake-up */ } close(fd); } TEST(TCPStreamerGTest, StreamerSurvivesClientDisconnectAndReconnect) { AppGuard guard; ReferenceT app = LoadApplication(BuildConfig(true, "Port = 44824", false, "")); ASSERT_TRUE(app.IsValid()); ReferenceT ds = ObjectRegistryDatabase::Instance()->Find("Test.Data.Streamer"); ASSERT_TRUE(ds.IsValid()); ASSERT_TRUE(app->PrepareNextState("State1") == ErrorManagement::NoError); for (int round = 0; round < 2; round++) { int fd = Connect(44824u); ASSERT_GE(fd, 0); Frame f; ASSERT_TRUE(ReadFrame(fd, f, 3000)); EXPECT_EQ(UDPS_TYPE_CONFIG, f.type) << "round " << round; close(fd); for (int i = 0; (i < 200) && (ds->GetClientCount() != 0u); i++) { SetCounter(ds, 1u); (void)ds->Synchronise(); Sleep::MSec(5u); } EXPECT_EQ(0u, ds->GetClientCount()); } } /*---------------------------------------------------------------------------*/ /* TCPStreamerClient */ /*---------------------------------------------------------------------------*/ TEST(TCPStreamerGTest, ClientDefaultsAndKeys) { { AppGuard guard; ReferenceT app = LoadApplication(BuildConfig(false, "", true, "")); EXPECT_TRUE(app.IsValid()); } { AppGuard guard; ReferenceT app = LoadApplication(BuildConfig( false, "", true, "ServerAddress = \"127.0.0.1\" Port = 44825 SilenceTimeout = 1.5 ReconnectDelay = 0.5")); EXPECT_TRUE(app.IsValid()); } } TEST(TCPStreamerGTest, ClientRejectsUdpOnlyKeys) { const char *const keys[] = { "MulticastGroup = \"239.0.0.1\"", "DataPort = 44501", "Interface = \"127.0.0.1\"", "KeepAliveInterval = 2" }; for (size_t i = 0u; i < sizeof(keys) / sizeof(keys[0]); i++) { AppGuard guard; ReferenceT app = LoadApplication(BuildConfig(false, "", true, std::string("Port = 44826\n") + keys[i])); EXPECT_FALSE(app.IsValid()) << keys[i]; } } TEST(TCPStreamerGTest, ClientReceivesFromRawServer) { AppGuard guard; uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); std::string extra = "Port = " + PortStr(port) + " ReconnectDelay = 0.1"; ReferenceT app = LoadApplication(BuildConfig(false, "", true, extra)); ASSERT_TRUE(app.IsValid()); ReferenceT ds = ObjectRegistryDatabase::Instance()->Find("Test.Data.Client"); ASSERT_TRUE(ds.IsValid()); ASSERT_TRUE(app->PrepareNextState("State1") == ErrorManagement::NoError); int cfd = AcceptOne(lfd, 3000); ASSERT_GE(cfd, 0); ASSERT_TRUE(SendAll(cfd, &MakeFrame(UDPS_TYPE_CONFIG, 0u, MakeConfigPayload())[0], UDPS_HEADER_SIZE + 4u + UDPS_SIGNAL_DESC_SIZE + 1u)); void *mem = NULL_PTR(void *); ASSERT_TRUE(ds->GetSignalMemoryBuffer(0u, 0u, mem)); /* Synchronise() only sees a Post that lands while it waits, so keep feeding. */ uint32 got = 0u; for (uint32 n = 0u; (n < 300u) && (got != 4242u); n++) { std::vector fr = MakeFrame(UDPS_TYPE_DATA, n + 1u, MakeDataPayload(4242u)); ASSERT_TRUE(SendAll(cfd, &fr[0], fr.size())); (void)ds->Synchronise(); memcpy(&got, mem, sizeof(got)); } EXPECT_EQ(4242u, got); EXPECT_TRUE(ds->IsConnected()); EXPECT_EQ(1u, ds->GetConnectionCount()); EXPECT_EQ(0u, ds->GetProtocolErrors()); /* Server goes away: the client must notice and reconnect by itself. */ close(cfd); int cfd2 = AcceptOne(lfd, 5000); EXPECT_GE(cfd2, 0); EXPECT_EQ(2u, ds->GetConnectionCount()); if (cfd2 >= 0) { close(cfd2); } close(lfd); } TEST(TCPStreamerGTest, ClientCountsProtocolErrorsAndReconnects) { AppGuard guard; uint16 port = 0u; int lfd = Listen(port); ASSERT_GE(lfd, 0); std::string extra = "Port = " + PortStr(port) + " ReconnectDelay = 0.1"; ReferenceT app = LoadApplication(BuildConfig(false, "", true, extra)); ASSERT_TRUE(app.IsValid()); ReferenceT ds = ObjectRegistryDatabase::Instance()->Find("Test.Data.Client"); ASSERT_TRUE(ds.IsValid()); ASSERT_TRUE(app->PrepareNextState("State1") == ErrorManagement::NoError); int cfd = AcceptOne(lfd, 3000); ASSERT_GE(cfd, 0); const uint8 garbage[UDPS_HEADER_SIZE] = { 0xDE, 0xAD, 0xBE, 0xEF }; ASSERT_TRUE(SendAll(cfd, garbage, sizeof(garbage))); EXPECT_TRUE(PeerClosed(cfd, 3000)); close(cfd); int cfd2 = AcceptOne(lfd, 5000); EXPECT_GE(cfd2, 0); EXPECT_EQ(1u, ds->GetProtocolErrors()); if (cfd2 >= 0) { close(cfd2); } close(lfd); } /*---------------------------------------------------------------------------*/ /* Streamer -> Client */ /*---------------------------------------------------------------------------*/ TEST(TCPStreamerGTest, StreamerToClientRoundTrip) { AppGuard guard; ReferenceT app = LoadApplication(BuildConfig( true, "Port = 44827", true, "Port = 44827 ReconnectDelay = 0.1")); ASSERT_TRUE(app.IsValid()); ReferenceT streamer = ObjectRegistryDatabase::Instance()->Find("Test.Data.Streamer"); ReferenceT client = ObjectRegistryDatabase::Instance()->Find("Test.Data.Client"); ASSERT_TRUE(streamer.IsValid()); ASSERT_TRUE(client.IsValid()); ASSERT_TRUE(app->PrepareNextState("State1") == ErrorManagement::NoError); void *mem = NULL_PTR(void *); ASSERT_TRUE(client->GetSignalMemoryBuffer(0u, 0u, mem)); uint32 got = 0u; for (uint32 n = 0u; (n < 600u) && (got != 777u); n++) { SetCounter(streamer, 777u); ASSERT_TRUE(streamer->Synchronise()); (void)client->Synchronise(); memcpy(&got, mem, sizeof(got)); } EXPECT_EQ(777u, got); EXPECT_TRUE(client->IsConnected()); EXPECT_EQ(1u, streamer->GetClientCount()); EXPECT_EQ(0u, client->GetProtocolErrors()); }