Files
Martino FerrariandClaude Opus 4.6 50d85363a3 feat(tcp): TCPStreamer DataSources, in-process TCPStreamService, stream multiplexing and TCPStreamJoin
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>
2026-11-06 13:41:27 +01:00

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());
}