Add TCPStreamer/TCPStreamerClient (TCP transport behind the UDPSServerI/UDPSClientI seam), an optional single in-process TCPStreamService serving many streams over worker threads, multiplexed SUBSCRIBE (comma list, stream index in fragmentIdx), and TCPStreamJoin, which merges several streams into one sample-and-hold stream paced by a master. StreamHub takes a comma list of streams over one connection. Includes GTests, E2E scenarios s58-s73 and docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
435 lines
16 KiB
C++
435 lines
16 KiB
C++
/**
|
|
* @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 <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <string>
|
|
|
|
#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<RealTimeApplication> 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<RealTimeApplication>();
|
|
}
|
|
ObjectRegistryDatabase *god = ObjectRegistryDatabase::Instance();
|
|
god->Purge();
|
|
if (!god->Initialise(cdb)) {
|
|
return ReferenceT<RealTimeApplication>();
|
|
}
|
|
ReferenceT<RealTimeApplication> app = god->Find("Test");
|
|
if (!app.IsValid() || !app->ConfigureApplication()) {
|
|
return ReferenceT<RealTimeApplication>();
|
|
}
|
|
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<unsigned>(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<TCPStreamer> &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<uint8> MakeConfigPayload() {
|
|
std::vector<uint8> 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<uint8> MakeDataPayload(uint32 value) {
|
|
std::vector<uint8> 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<RealTimeApplication> app = LoadApplication(BuildConfig(true, "", false, ""));
|
|
ASSERT_TRUE(app.IsValid());
|
|
ReferenceT<TCPStreamer> 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<RealTimeApplication> app =
|
|
LoadApplication(BuildConfig(true, std::string("Port = 44820\n") + keys[i], false, ""));
|
|
EXPECT_FALSE(app.IsValid()) << keys[i];
|
|
}
|
|
}
|
|
|
|
TEST(TCPStreamerGTest, StreamerAcceptsAndValidatesTcpKeys) {
|
|
{
|
|
AppGuard guard;
|
|
ReferenceT<RealTimeApplication> 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<RealTimeApplication> app =
|
|
LoadApplication(BuildConfig(true, std::string("Port = 44822 ") + bad[i], false, ""));
|
|
EXPECT_FALSE(app.IsValid()) << bad[i];
|
|
}
|
|
}
|
|
|
|
TEST(TCPStreamerGTest, StreamerDeliversConfigThenData) {
|
|
AppGuard guard;
|
|
ReferenceT<RealTimeApplication> app = LoadApplication(BuildConfig(true, "Port = 44823", false, ""));
|
|
ASSERT_TRUE(app.IsValid());
|
|
ReferenceT<TCPStreamer> 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<RealTimeApplication> app = LoadApplication(BuildConfig(true, "Port = 44824", false, ""));
|
|
ASSERT_TRUE(app.IsValid());
|
|
ReferenceT<TCPStreamer> 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<RealTimeApplication> app = LoadApplication(BuildConfig(false, "", true, ""));
|
|
EXPECT_TRUE(app.IsValid());
|
|
}
|
|
{
|
|
AppGuard guard;
|
|
ReferenceT<RealTimeApplication> 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<RealTimeApplication> 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<RealTimeApplication> app = LoadApplication(BuildConfig(false, "", true, extra));
|
|
ASSERT_TRUE(app.IsValid());
|
|
ReferenceT<TCPStreamerClient> 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<uint8> 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<RealTimeApplication> app = LoadApplication(BuildConfig(false, "", true, extra));
|
|
ASSERT_TRUE(app.IsValid());
|
|
ReferenceT<TCPStreamerClient> 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<RealTimeApplication> app = LoadApplication(BuildConfig(
|
|
true, "Port = 44827", true, "Port = 44827 ReconnectDelay = 0.1"));
|
|
ASSERT_TRUE(app.IsValid());
|
|
ReferenceT<TCPStreamer> streamer = ObjectRegistryDatabase::Instance()->Find("Test.Data.Streamer");
|
|
ReferenceT<TCPStreamerClient> 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());
|
|
}
|