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>
1105 lines
39 KiB
C++
1105 lines
39 KiB
C++
/**
|
|
* @file TCPStreamServiceGTest.cpp
|
|
* @brief GTests for TCPStreamService / TCPSServiceStream and the Service mode
|
|
* of the TCPStreamer DataSource.
|
|
*
|
|
* Most tests drive a TCPSServiceStream directly (no RealTimeApplication) with
|
|
* raw sockets, which makes the producer side deterministic. The last tests use
|
|
* the TCPStreamer / TCPStreamerClient DataSources on top of a service.
|
|
*/
|
|
|
|
#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 "TCPSServiceStream.h"
|
|
#include "TCPStreamJoin.h"
|
|
#include "TCPStreamService.h"
|
|
#include "TCPStreamer.h"
|
|
#include "TCPStreamerClient.h"
|
|
#include "TCPTestUtil.h"
|
|
#include "UDPSProtocol.h"
|
|
|
|
using namespace MARTe;
|
|
using namespace tcptest;
|
|
|
|
namespace {
|
|
|
|
class SvcTestWriterGAM : public GAM {
|
|
public:
|
|
CLASS_REGISTER_DECLARATION()
|
|
SvcTestWriterGAM() : GAM() {}
|
|
virtual ~SvcTestWriterGAM() {}
|
|
virtual bool Setup() { return true; }
|
|
virtual bool Execute() { return true; }
|
|
};
|
|
CLASS_REGISTER(SvcTestWriterGAM, "1.0")
|
|
|
|
class SvcTestReaderGAM : public GAM {
|
|
public:
|
|
CLASS_REGISTER_DECLARATION()
|
|
SvcTestReaderGAM() : GAM() {}
|
|
virtual ~SvcTestReaderGAM() {}
|
|
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(SvcTestReaderGAM, "1.0")
|
|
|
|
std::string PortStr(uint16 port) {
|
|
char buf[16];
|
|
snprintf(buf, sizeof(buf), "%u", static_cast<unsigned>(port));
|
|
return buf;
|
|
}
|
|
|
|
std::vector<uint8> NameBytes(const char *s) {
|
|
return std::vector<uint8>(reinterpret_cast<const uint8 *>(s),
|
|
reinterpret_cast<const uint8 *>(s) + strlen(s));
|
|
}
|
|
|
|
/** Connect and send a SUBSCRIBE for @p name. */
|
|
int Subscribe(uint16 port, const char *name, int rcvBuf = 0) {
|
|
int fd = Connect(port, rcvBuf);
|
|
if (fd >= 0) {
|
|
std::vector<uint8> f = MakeFrame(UDPS_TYPE_SUBSCRIBE, 0u, NameBytes(name));
|
|
if (!SendAll(fd, &f[0], f.size())) {
|
|
close(fd);
|
|
fd = -1;
|
|
}
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
/** A service created programmatically and published under the name "Svc". */
|
|
struct ServiceFixture {
|
|
explicit ServiceFixture(const char *extra = "") : ok(false) {
|
|
ObjectRegistryDatabase::Instance()->Purge();
|
|
std::string cfg = std::string("Class = TCPStreamService\nPort = 0\n") + extra + "\n";
|
|
ConfigurationDatabase cdb;
|
|
StreamString s = cfg.c_str();
|
|
(void)s.Seek(0LLU);
|
|
StandardParser parser(s, cdb);
|
|
if (!parser.Parse() || !cdb.MoveToRoot()) {
|
|
return;
|
|
}
|
|
svc = ReferenceT<TCPStreamService>(new TCPStreamService());
|
|
svc->SetName("Svc");
|
|
// Class = ... is consumed by the registry, not by the object itself.
|
|
(void)cdb.Delete("Class");
|
|
ok = svc->Initialise(cdb);
|
|
if (ok) {
|
|
ok = ObjectRegistryDatabase::Instance()->Insert(svc);
|
|
}
|
|
}
|
|
~ServiceFixture() {
|
|
svc = ReferenceT<TCPStreamService>();
|
|
ObjectRegistryDatabase::Instance()->Purge();
|
|
}
|
|
uint16 Port() const { return svc->GetBoundPort(); }
|
|
|
|
bool ok;
|
|
ReferenceT<TCPStreamService> svc;
|
|
};
|
|
|
|
bool WaitClients(const TCPSServiceStream &st, uint32 n, int timeoutMs = 3000) {
|
|
for (int i = 0; i < timeoutMs / 5; i++) {
|
|
if (st.GetClientCount() >= n) {
|
|
return true;
|
|
}
|
|
Sleep::MSec(5u);
|
|
}
|
|
return st.GetClientCount() >= n;
|
|
}
|
|
|
|
std::vector<uint8> Payload(uint32 n, uint8 seed) {
|
|
return Pattern(n, seed);
|
|
}
|
|
|
|
bool StartStream(TCPSServiceStream &st) {
|
|
ConfigurationDatabase cdb;
|
|
if (!cdb.Write("MaxPayloadSize", static_cast<uint32>(60000u)) || !cdb.MoveToRoot()) {
|
|
return false;
|
|
}
|
|
return st.Initialise(cdb) && st.Start();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(TCPStreamServiceGTest, ClassIsRegistered) {
|
|
EXPECT_TRUE(ClassRegistryDatabase::Instance()->Find("TCPStreamService") !=
|
|
NULL_PTR(const ClassRegistryItem *));
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, DefaultsAndEphemeralPort) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
EXPECT_NE(0u, fx.Port());
|
|
EXPECT_EQ(2u, fx.svc->GetWorkerCount());
|
|
EXPECT_EQ(0u, fx.svc->GetStreamCount());
|
|
EXPECT_EQ(0u, fx.svc->GetClientCount());
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, RejectsInvalidParameters) {
|
|
const char *const bad[] = {
|
|
"Workers = 0", "Workers = 64", "MaxClients = 0", "MaxStreams = 0",
|
|
"ClientQueueDepth = 0", "HandshakeTimeout = 0", "Port = 70000"
|
|
};
|
|
for (size_t i = 0u; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
|
ServiceFixture fx(bad[i]);
|
|
EXPECT_FALSE(fx.ok) << bad[i];
|
|
}
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, SubscriberGetsConfigThenData) {
|
|
ServiceFixture fx("ClientQueueDepth = 32");
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream st("Svc", "Alpha");
|
|
ASSERT_TRUE(StartStream(st));
|
|
EXPECT_EQ(1u, fx.svc->GetStreamCount());
|
|
EXPECT_EQ(fx.Port(), st.GetPort());
|
|
|
|
ASSERT_TRUE(st.SendConfig(&Payload(150u, 1u)[0], 150u));
|
|
int fd = Subscribe(fx.Port(), "Alpha");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitClients(st, 1u));
|
|
EXPECT_TRUE(st.HasClients());
|
|
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
EXPECT_EQ(Payload(150u, 1u), f.payload);
|
|
|
|
for (uint32 c = 1u; c <= 20u; c++) {
|
|
ASSERT_TRUE(st.SendData(c, &Payload(64u, static_cast<uint8>(c))[0], 64u));
|
|
}
|
|
for (uint32 c = 1u; c <= 20u; c++) {
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
EXPECT_EQ(UDPS_TYPE_DATA, f.type);
|
|
EXPECT_EQ(c, f.counter);
|
|
EXPECT_EQ(Payload(64u, static_cast<uint8>(c)), f.payload);
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, ConfigPublishedAfterSubscribeIsDelivered) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream st("Svc", "Alpha");
|
|
ASSERT_TRUE(StartStream(st));
|
|
int fd = Subscribe(fx.Port(), "Alpha");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitClients(st, 1u));
|
|
|
|
ASSERT_TRUE(st.SendConfig(&Payload(40u, 9u)[0], 40u));
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
EXPECT_EQ(40u, f.payload.size());
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, UnknownStreamIsRejected) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream st("Svc", "Alpha");
|
|
ASSERT_TRUE(StartStream(st));
|
|
int fd = Subscribe(fx.Port(), "Nope");
|
|
ASSERT_GE(fd, 0);
|
|
EXPECT_TRUE(PeerClosed(fd, 3000));
|
|
EXPECT_EQ(1u, fx.svc->GetRejectedSubscriptions());
|
|
EXPECT_EQ(0u, st.GetClientCount());
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, GarbageHandshakeIsRejected) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
int fd = Connect(fx.Port());
|
|
ASSERT_GE(fd, 0);
|
|
std::vector<uint8> junk(UDPS_HEADER_SIZE, 0xAAu);
|
|
ASSERT_TRUE(SendAll(fd, &junk[0], junk.size()));
|
|
EXPECT_TRUE(PeerClosed(fd, 3000));
|
|
EXPECT_EQ(1u, fx.svc->GetRejectedSubscriptions());
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, WrongFrameTypeFirstIsRejected) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
int fd = Connect(fx.Port());
|
|
ASSERT_GE(fd, 0);
|
|
std::vector<uint8> f = MakeFrame(UDPS_TYPE_DATA, 0u, NameBytes("Alpha"));
|
|
ASSERT_TRUE(SendAll(fd, &f[0], f.size()));
|
|
EXPECT_TRUE(PeerClosed(fd, 3000));
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, SilentConnectionTimesOut) {
|
|
ServiceFixture fx("HandshakeTimeout = 1");
|
|
ASSERT_TRUE(fx.ok);
|
|
int fd = Connect(fx.Port());
|
|
ASSERT_GE(fd, 0);
|
|
EXPECT_TRUE(PeerClosed(fd, 4000));
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, StreamsAreIsolated) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream a("Svc", "A");
|
|
TCPSServiceStream b("Svc", "B");
|
|
ASSERT_TRUE(StartStream(a));
|
|
ASSERT_TRUE(StartStream(b));
|
|
EXPECT_EQ(2u, fx.svc->GetStreamCount());
|
|
ASSERT_TRUE(a.SendConfig(&Payload(20u, 1u)[0], 20u));
|
|
ASSERT_TRUE(b.SendConfig(&Payload(30u, 2u)[0], 30u));
|
|
|
|
int fa = Subscribe(fx.Port(), "A");
|
|
int fb = Subscribe(fx.Port(), "B");
|
|
ASSERT_GE(fa, 0);
|
|
ASSERT_GE(fb, 0);
|
|
ASSERT_TRUE(WaitClients(a, 1u));
|
|
ASSERT_TRUE(WaitClients(b, 1u));
|
|
EXPECT_EQ(2u, fx.svc->GetClientCount());
|
|
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fa, f, 2000));
|
|
EXPECT_EQ(20u, f.payload.size());
|
|
ASSERT_TRUE(ReadFrame(fb, f, 2000));
|
|
EXPECT_EQ(30u, f.payload.size());
|
|
|
|
ASSERT_TRUE(a.SendData(1u, &Payload(8u, 0xA0u)[0], 8u));
|
|
ASSERT_TRUE(ReadFrame(fa, f, 2000));
|
|
EXPECT_EQ(Payload(8u, 0xA0u), f.payload);
|
|
EXPECT_FALSE(ReadFrame(fb, f, 200)) << "stream B must not see stream A data";
|
|
|
|
ASSERT_TRUE(b.SendData(1u, &Payload(8u, 0xB0u)[0], 8u));
|
|
ASSERT_TRUE(ReadFrame(fb, f, 2000));
|
|
EXPECT_EQ(Payload(8u, 0xB0u), f.payload);
|
|
close(fa);
|
|
close(fb);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, SeveralSubscribersOfOneStream) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream st("Svc", "A");
|
|
ASSERT_TRUE(StartStream(st));
|
|
ASSERT_TRUE(st.SendConfig(&Payload(10u, 1u)[0], 10u));
|
|
int fds[4];
|
|
for (int i = 0; i < 4; i++) {
|
|
fds[i] = Subscribe(fx.Port(), "A");
|
|
ASSERT_GE(fds[i], 0);
|
|
}
|
|
ASSERT_TRUE(WaitClients(st, 4u));
|
|
ASSERT_TRUE(st.SendData(7u, &Payload(16u, 3u)[0], 16u));
|
|
for (int i = 0; i < 4; i++) {
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fds[i], f, 2000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
ASSERT_TRUE(ReadFrame(fds[i], f, 2000));
|
|
EXPECT_EQ(UDPS_TYPE_DATA, f.type);
|
|
EXPECT_EQ(7u, f.counter);
|
|
close(fds[i]);
|
|
}
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, StopDisconnectsSubscribersAndStreamCanRestart) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream st("Svc", "A");
|
|
ASSERT_TRUE(StartStream(st));
|
|
int fd = Subscribe(fx.Port(), "A");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitClients(st, 1u));
|
|
|
|
EXPECT_TRUE(st.Stop());
|
|
EXPECT_EQ(0u, fx.svc->GetStreamCount());
|
|
EXPECT_TRUE(PeerClosed(fd, 3000));
|
|
close(fd);
|
|
|
|
ASSERT_TRUE(st.Start());
|
|
ASSERT_TRUE(st.SendConfig(&Payload(12u, 4u)[0], 12u));
|
|
fd = Subscribe(fx.Port(), "A");
|
|
ASSERT_GE(fd, 0);
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, DuplicateStreamNameIsRefused) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream a("Svc", "Same");
|
|
TCPSServiceStream b("Svc", "Same");
|
|
ASSERT_TRUE(StartStream(a));
|
|
EXPECT_FALSE(StartStream(b));
|
|
EXPECT_EQ(1u, fx.svc->GetStreamCount());
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, MissingServiceFailsStart) {
|
|
ObjectRegistryDatabase::Instance()->Purge();
|
|
TCPSServiceStream st("NoSuchService", "A");
|
|
EXPECT_FALSE(StartStream(st));
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, MaxClientsIsEnforced) {
|
|
ServiceFixture fx("MaxClients = 1");
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream st("Svc", "A");
|
|
ASSERT_TRUE(StartStream(st));
|
|
int first = Subscribe(fx.Port(), "A");
|
|
ASSERT_GE(first, 0);
|
|
ASSERT_TRUE(WaitClients(st, 1u));
|
|
int second = Connect(fx.Port());
|
|
ASSERT_GE(second, 0);
|
|
EXPECT_TRUE(PeerClosed(second, 3000));
|
|
close(second);
|
|
EXPECT_EQ(1u, st.GetClientCount());
|
|
close(first);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, ClientCountDropsWhenSubscriberLeaves) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream st("Svc", "A");
|
|
ASSERT_TRUE(StartStream(st));
|
|
int fd = Subscribe(fx.Port(), "A");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitClients(st, 1u));
|
|
close(fd);
|
|
for (int i = 0; (i < 400) && (st.GetClientCount() != 0u); i++) {
|
|
Sleep::MSec(5u);
|
|
}
|
|
EXPECT_EQ(0u, st.GetClientCount());
|
|
EXPECT_FALSE(st.HasClients());
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, SlowSubscriberOnlyLosesItsOwnFrames) {
|
|
ServiceFixture fx("ClientQueueDepth = 16 ClientStallTimeout = 1 SendBufferSize = 4096");
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream st("Svc", "A");
|
|
ASSERT_TRUE(StartStream(st));
|
|
|
|
int slow = Subscribe(fx.Port(), "A", 2048); /* never read */
|
|
int fast = Subscribe(fx.Port(), "A");
|
|
ASSERT_GE(slow, 0);
|
|
ASSERT_GE(fast, 0);
|
|
ASSERT_TRUE(WaitClients(st, 2u));
|
|
|
|
const uint32 frames = 3000u;
|
|
const std::vector<uint8> body = Payload(2000u, 5u);
|
|
for (uint32 c = 1u; c <= frames; c++) {
|
|
ASSERT_TRUE(st.SendData(c, &body[0], static_cast<uint32>(body.size())));
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fast, f, 3000)) << "fast subscriber starved at frame " << c;
|
|
ASSERT_EQ(c, f.counter);
|
|
}
|
|
EXPECT_GT(st.GetClientDrops(), 0u);
|
|
EXPECT_EQ(2u, st.GetClientCount());
|
|
|
|
/* The stalled subscriber is evicted; the fast one is untouched. */
|
|
for (int i = 0; (i < 600) && (fx.svc->GetStallEvictions() == 0u); i++) {
|
|
Sleep::MSec(10u);
|
|
}
|
|
EXPECT_EQ(1u, fx.svc->GetStallEvictions());
|
|
ASSERT_TRUE(WaitClients(st, 1u));
|
|
for (int i = 0; (i < 200) && (st.GetClientCount() != 1u); i++) {
|
|
Sleep::MSec(10u);
|
|
}
|
|
EXPECT_EQ(1u, st.GetClientCount());
|
|
ASSERT_TRUE(st.SendData(frames + 1u, &body[0], static_cast<uint32>(body.size())));
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fast, f, 3000));
|
|
EXPECT_EQ(frames + 1u, f.counter);
|
|
close(slow);
|
|
close(fast);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, MultiplexedConnectionTagsFramesWithStreamIndex) {
|
|
ServiceFixture fx("ClientQueueDepth = 32");
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream a("Svc", "A");
|
|
TCPSServiceStream b("Svc", "B");
|
|
ASSERT_TRUE(StartStream(a));
|
|
ASSERT_TRUE(StartStream(b));
|
|
ASSERT_TRUE(a.SendConfig(&Payload(20u, 1u)[0], 20u));
|
|
ASSERT_TRUE(b.SendConfig(&Payload(30u, 2u)[0], 30u));
|
|
|
|
/* List order, not registration order, defines the index. */
|
|
int fd = Subscribe(fx.Port(), "B,A");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitClients(a, 1u));
|
|
ASSERT_TRUE(WaitClients(b, 1u));
|
|
EXPECT_EQ(1u, a.GetClientCount());
|
|
EXPECT_EQ(1u, b.GetClientCount());
|
|
|
|
uint32 configs = 0u;
|
|
for (int i = 0; i < 2; i++) {
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fd, f, 2000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
EXPECT_EQ(1u, f.totalFragments);
|
|
if (f.fragmentIdx == 0u) {
|
|
EXPECT_EQ(30u, f.payload.size()); /* B */
|
|
} else {
|
|
EXPECT_EQ(1u, f.fragmentIdx);
|
|
EXPECT_EQ(20u, f.payload.size()); /* A */
|
|
}
|
|
configs++;
|
|
}
|
|
EXPECT_EQ(2u, configs);
|
|
|
|
ASSERT_TRUE(a.SendData(1u, &Payload(8u, 0xA0u)[0], 8u));
|
|
ASSERT_TRUE(b.SendData(1u, &Payload(8u, 0xB0u)[0], 8u));
|
|
for (int i = 0; i < 2; i++) {
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fd, f, 2000));
|
|
EXPECT_EQ(UDPS_TYPE_DATA, f.type);
|
|
EXPECT_EQ((f.fragmentIdx == 0u) ? Payload(8u, 0xB0u) : Payload(8u, 0xA0u), f.payload);
|
|
EXPECT_LT(f.fragmentIdx, 2u);
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, SingleNameConnectionStillUsesIndexZero) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream a("Svc", "A");
|
|
ASSERT_TRUE(StartStream(a));
|
|
int fd = Subscribe(fx.Port(), "A");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitClients(a, 1u));
|
|
ASSERT_TRUE(a.SendData(1u, &Payload(8u, 1u)[0], 8u));
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fd, f, 2000));
|
|
EXPECT_EQ(0u, f.fragmentIdx);
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, MultiplexedBadListsAreRejected) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream a("Svc", "A");
|
|
TCPSServiceStream b("Svc", "B");
|
|
ASSERT_TRUE(StartStream(a));
|
|
ASSERT_TRUE(StartStream(b));
|
|
const char *const bad[] = { "A,Nope", "A,A", "A,,B", "A,B,", ",A", "A, B" };
|
|
for (size_t i = 0u; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
|
int fd = Subscribe(fx.Port(), bad[i]);
|
|
ASSERT_GE(fd, 0);
|
|
EXPECT_TRUE(PeerClosed(fd, 3000)) << bad[i];
|
|
close(fd);
|
|
}
|
|
/* A failed attach must not leave half a subscription behind. */
|
|
Sleep::MSec(50u);
|
|
EXPECT_EQ(0u, a.GetClientCount());
|
|
EXPECT_EQ(0u, b.GetClientCount());
|
|
EXPECT_EQ(sizeof(bad) / sizeof(bad[0]), static_cast<size_t>(fx.svc->GetRejectedSubscriptions()));
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, StreamNameWithCommaIsRefused) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream st("Svc", "A,B");
|
|
EXPECT_FALSE(StartStream(st));
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, StoppingOneStreamClosesMultiplexedConnections) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream a("Svc", "A");
|
|
TCPSServiceStream b("Svc", "B");
|
|
ASSERT_TRUE(StartStream(a));
|
|
ASSERT_TRUE(StartStream(b));
|
|
int fd = Subscribe(fx.Port(), "A,B");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitClients(a, 1u));
|
|
ASSERT_TRUE(WaitClients(b, 1u));
|
|
EXPECT_TRUE(a.Stop());
|
|
EXPECT_TRUE(PeerClosed(fd, 3000));
|
|
for (int i = 0; (i < 400) && (b.GetClientCount() != 0u); i++) {
|
|
Sleep::MSec(5u);
|
|
}
|
|
EXPECT_EQ(0u, b.GetClientCount());
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, FastStreamCannotEvictSlowStreamFrames) {
|
|
ServiceFixture fx("ClientQueueDepth = 4 ClientStallTimeout = 60 SendBufferSize = 4096");
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream fast("Svc", "Fast");
|
|
TCPSServiceStream slow("Svc", "Slow");
|
|
ASSERT_TRUE(StartStream(fast));
|
|
ASSERT_TRUE(StartStream(slow));
|
|
int fd = Subscribe(fx.Port(), "Fast,Slow", 2048); /* not read while filling */
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitClients(fast, 1u));
|
|
ASSERT_TRUE(WaitClients(slow, 1u));
|
|
|
|
const std::vector<uint8> big = Payload(2000u, 7u);
|
|
for (uint32 c = 1u; c <= 300u; c++) {
|
|
ASSERT_TRUE(fast.SendData(c, &big[0], static_cast<uint32>(big.size())));
|
|
}
|
|
for (uint32 c = 1u; c <= 3u; c++) {
|
|
ASSERT_TRUE(slow.SendData(c, &Payload(16u, static_cast<uint8>(c))[0], 16u));
|
|
}
|
|
|
|
uint32 nFast = 0u;
|
|
uint32 nSlow = 0u;
|
|
Frame f;
|
|
while (ReadFrame(fd, f, 500)) {
|
|
if (f.fragmentIdx == 0u) {
|
|
nFast++;
|
|
} else {
|
|
EXPECT_EQ(1u, f.fragmentIdx);
|
|
nSlow++;
|
|
}
|
|
}
|
|
EXPECT_EQ(3u, nSlow) << "slow stream frames were evicted by the fast stream";
|
|
EXPECT_LT(nFast, 300u);
|
|
EXPECT_GT(fast.GetClientDrops(), 0u);
|
|
EXPECT_EQ(0u, slow.GetClientDrops());
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, ManyStreamsManyClientsAcrossWorkers) {
|
|
ServiceFixture fx("Workers = 3 ClientQueueDepth = 64");
|
|
ASSERT_TRUE(fx.ok);
|
|
const int numStreams = 4;
|
|
TCPSServiceStream *streams[numStreams];
|
|
const char *names[numStreams] = { "S0", "S1", "S2", "S3" };
|
|
int fds[numStreams][3];
|
|
for (int s = 0; s < numStreams; s++) {
|
|
streams[s] = new TCPSServiceStream("Svc", names[s]);
|
|
ASSERT_TRUE(StartStream(*streams[s]));
|
|
ASSERT_TRUE(streams[s]->SendConfig(&Payload(8u, static_cast<uint8>(s))[0], 8u));
|
|
for (int c = 0; c < 3; c++) {
|
|
fds[s][c] = Subscribe(fx.Port(), names[s]);
|
|
ASSERT_GE(fds[s][c], 0);
|
|
}
|
|
ASSERT_TRUE(WaitClients(*streams[s], 3u));
|
|
}
|
|
EXPECT_EQ(12u, fx.svc->GetClientCount());
|
|
for (uint32 n = 1u; n <= 50u; n++) {
|
|
for (int s = 0; s < numStreams; s++) {
|
|
ASSERT_TRUE(streams[s]->SendData(n, &Payload(32u, static_cast<uint8>(s))[0], 32u));
|
|
}
|
|
}
|
|
for (int s = 0; s < numStreams; s++) {
|
|
for (int c = 0; c < 3; c++) {
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fds[s][c], f, 2000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
for (uint32 n = 1u; n <= 50u; n++) {
|
|
ASSERT_TRUE(ReadFrame(fds[s][c], f, 2000));
|
|
EXPECT_EQ(n, f.counter);
|
|
EXPECT_EQ(Payload(32u, static_cast<uint8>(s)), f.payload);
|
|
}
|
|
close(fds[s][c]);
|
|
}
|
|
}
|
|
for (int s = 0; s < numStreams; s++) {
|
|
delete streams[s];
|
|
}
|
|
}
|
|
|
|
/*---------------------------------------------------------------------------*/
|
|
/* DataSources on top of the service */
|
|
/*---------------------------------------------------------------------------*/
|
|
|
|
namespace {
|
|
|
|
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;
|
|
}
|
|
|
|
struct AppGuard {
|
|
~AppGuard() { ObjectRegistryDatabase::Instance()->Purge(); }
|
|
};
|
|
|
|
/**
|
|
* Application with a service at the root and two service-mode streamers
|
|
* (Fast, Slow) and, optionally, one client DataSource.
|
|
*/
|
|
std::string ServiceAppConfig(uint16 port, const std::string &fastExtra,
|
|
const std::string &clientExtra) {
|
|
std::string cfg =
|
|
"+Service = { Class = TCPStreamService Port = " + PortStr(port) + " Workers = 2 }\n"
|
|
"+Test = {\n Class = RealTimeApplication\n +Functions = {\n Class = ReferenceContainer\n"
|
|
" +WFast = { Class = SvcTestWriterGAM\n"
|
|
" OutputSignals = { Counter = { DataSource = Fast Type = uint32 } } }\n"
|
|
" +WSlow = { Class = SvcTestWriterGAM\n"
|
|
" OutputSignals = { Counter = { DataSource = Slow Type = uint32 } } }\n";
|
|
std::string functions = " WFast WSlow";
|
|
if (!clientExtra.empty()) {
|
|
cfg += " +Reader = { Class = SvcTestReaderGAM\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 (!clientExtra.empty()) {
|
|
cfg += " DefaultDataSource = DDB\n +DDB = { Class = GAMDataSource }\n";
|
|
}
|
|
cfg += " +Timings = { Class = TimingDataSource }\n"
|
|
" +Fast = { Class = TCPStreamer Service = Service StreamName = FastStream " + fastExtra +
|
|
"\n Signals = { Counter = { Type = uint32 } } }\n"
|
|
" +Slow = { Class = TCPStreamer Service = Service\n"
|
|
" Signals = { Counter = { Type = uint32 } } }\n";
|
|
if (!clientExtra.empty()) {
|
|
cfg += " +Client = { Class = TCPStreamerClient Port = " + PortStr(port) + " " + 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;
|
|
}
|
|
|
|
void SetCounter(ReferenceT<TCPStreamer> &ds, uint32 v) {
|
|
void *mem = NULL_PTR(void *);
|
|
ASSERT_TRUE(ds->GetSignalMemoryBuffer(0u, 0u, mem));
|
|
memcpy(mem, &v, sizeof(v));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(TCPStreamServiceGTest, StreamerServiceModeRejectsServiceOwnedKeys) {
|
|
const char *const keys[] = {
|
|
"Port = 44900", "MaxClients = 4", "ClientQueueDepth = 4", "ClientStallTimeout = 1",
|
|
"SendBufferSize = 4096"
|
|
};
|
|
for (size_t i = 0u; i < sizeof(keys) / sizeof(keys[0]); i++) {
|
|
AppGuard guard;
|
|
ReferenceT<RealTimeApplication> app = LoadApplication(ServiceAppConfig(44901u, keys[i], ""));
|
|
EXPECT_FALSE(app.IsValid()) << keys[i];
|
|
}
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, StreamerServiceModeStartsWithoutService) {
|
|
AppGuard guard;
|
|
std::string cfg = ServiceAppConfig(44902u, "", "");
|
|
/* Point the streamers at an object that does not exist. */
|
|
size_t pos;
|
|
while ((pos = cfg.find("Service = Service")) != std::string::npos) {
|
|
cfg.replace(pos, strlen("Service = Service"), "Service = Missing");
|
|
}
|
|
ReferenceT<RealTimeApplication> app = LoadApplication(cfg);
|
|
ASSERT_TRUE(app.IsValid());
|
|
EXPECT_FALSE(app->PrepareNextState("State1") == ErrorManagement::NoError);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, StreamerServiceModeAccessors) {
|
|
AppGuard guard;
|
|
ReferenceT<RealTimeApplication> app = LoadApplication(ServiceAppConfig(44903u, "", ""));
|
|
ASSERT_TRUE(app.IsValid());
|
|
ReferenceT<TCPStreamer> fast = ObjectRegistryDatabase::Instance()->Find("Test.Data.Fast");
|
|
ReferenceT<TCPStreamer> slow = ObjectRegistryDatabase::Instance()->Find("Test.Data.Slow");
|
|
ASSERT_TRUE(fast.IsValid());
|
|
ASSERT_TRUE(slow.IsValid());
|
|
EXPECT_TRUE(fast->UsesService());
|
|
ASSERT_TRUE(app->PrepareNextState("State1") == ErrorManagement::NoError);
|
|
ReferenceT<TCPStreamService> svc = ObjectRegistryDatabase::Instance()->Find("Service");
|
|
ASSERT_TRUE(svc.IsValid());
|
|
EXPECT_EQ(2u, svc->GetStreamCount());
|
|
EXPECT_EQ(44903u, fast->GetBoundPort());
|
|
EXPECT_EQ(0u, fast->GetClientCount());
|
|
|
|
/* StreamName defaults to the DataSource name. */
|
|
int fd = Subscribe(44903u, "Slow");
|
|
ASSERT_GE(fd, 0);
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
close(fd);
|
|
fd = Subscribe(44903u, "FastStream");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, ServiceStreamerToClientRoundTrip) {
|
|
AppGuard guard;
|
|
ReferenceT<RealTimeApplication> app = LoadApplication(ServiceAppConfig(
|
|
44904u, "", "StreamName = FastStream ReconnectDelay = 0.1"));
|
|
ASSERT_TRUE(app.IsValid());
|
|
ReferenceT<TCPStreamer> fast = ObjectRegistryDatabase::Instance()->Find("Test.Data.Fast");
|
|
ReferenceT<TCPStreamer> slow = ObjectRegistryDatabase::Instance()->Find("Test.Data.Slow");
|
|
ReferenceT<TCPStreamerClient> client = ObjectRegistryDatabase::Instance()->Find("Test.Data.Client");
|
|
ASSERT_TRUE(fast.IsValid());
|
|
ASSERT_TRUE(slow.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 != 111u); n++) {
|
|
SetCounter(fast, 111u);
|
|
SetCounter(slow, 222u); /* must never reach the client */
|
|
ASSERT_TRUE(fast->Synchronise());
|
|
ASSERT_TRUE(slow->Synchronise());
|
|
(void)client->Synchronise();
|
|
memcpy(&got, mem, sizeof(got));
|
|
EXPECT_NE(222u, got);
|
|
}
|
|
EXPECT_EQ(111u, got);
|
|
EXPECT_TRUE(client->IsConnected());
|
|
EXPECT_EQ(1u, fast->GetClientCount());
|
|
EXPECT_EQ(0u, slow->GetClientCount());
|
|
EXPECT_EQ(0u, client->GetProtocolErrors());
|
|
}
|
|
|
|
/*---------------------------------------------------------------------------*/
|
|
/* TCPStreamJoin */
|
|
/*---------------------------------------------------------------------------*/
|
|
|
|
namespace {
|
|
|
|
struct SigSpec {
|
|
const char *name;
|
|
uint32 timeIdx;
|
|
};
|
|
|
|
/** CONFIG payload with the given signals, publish mode and HRT frequency. */
|
|
std::vector<uint8> MakeConfig(const std::vector<SigSpec> &sigs, uint8 mode, uint64 hrt, bool withHrt = true) {
|
|
std::vector<uint8> p(4u + sigs.size() * UDPS_SIGNAL_DESC_SIZE + 1u + (withHrt ? 8u : 0u), 0u);
|
|
uint32 n = static_cast<uint32>(sigs.size());
|
|
memcpy(&p[0], &n, 4u);
|
|
for (size_t i = 0u; i < sigs.size(); i++) {
|
|
UDPSSignalDescriptor d;
|
|
memset(&d, 0, sizeof(d));
|
|
strncpy(d.name, sigs[i].name, UDPS_MAX_SIGNAL_NAME - 1u);
|
|
d.numRows = 1u;
|
|
d.numCols = 1u;
|
|
d.timeSignalIdx = sigs[i].timeIdx;
|
|
memcpy(&p[4u + i * UDPS_SIGNAL_DESC_SIZE], &d, UDPS_SIGNAL_DESC_SIZE);
|
|
}
|
|
p[4u + sigs.size() * UDPS_SIGNAL_DESC_SIZE] = mode;
|
|
if (withHrt) {
|
|
memcpy(&p[4u + sigs.size() * UDPS_SIGNAL_DESC_SIZE + 1u], &hrt, 8u);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
/** DATA payload: 8-byte timestamp then @p body. */
|
|
std::vector<uint8> MakeData(uint64 ts, const std::vector<uint8> &body) {
|
|
std::vector<uint8> p(8u + body.size());
|
|
memcpy(&p[0], &ts, 8u);
|
|
if (!body.empty()) {
|
|
memcpy(&p[8], &body[0], body.size());
|
|
}
|
|
return p;
|
|
}
|
|
|
|
/** Creates a TCPStreamJoin the way the configuration loader would. */
|
|
ReferenceT<TCPStreamJoin> MakeJoin(const std::string ¶ms, bool &ok) {
|
|
ConfigurationDatabase cdb;
|
|
StreamString s = (params + "\n").c_str();
|
|
(void)s.Seek(0LLU);
|
|
StandardParser parser(s, cdb);
|
|
ReferenceT<TCPStreamJoin> j(new TCPStreamJoin());
|
|
ok = parser.Parse() && cdb.MoveToRoot();
|
|
if (ok) {
|
|
j->SetName("Join");
|
|
ok = j->Initialise(cdb);
|
|
}
|
|
return j;
|
|
}
|
|
|
|
bool WaitSvcClients(const TCPStreamService *svc, uint32 n, int timeoutMs = 3000) {
|
|
for (int i = 0; i < timeoutMs / 5; i++) {
|
|
if (svc->GetClientCount() >= n) {
|
|
return true;
|
|
}
|
|
Sleep::MSec(5u);
|
|
}
|
|
return svc->GetClientCount() >= n;
|
|
}
|
|
|
|
const uint64 kHrt = 1000000000ull;
|
|
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, JoinClassIsRegistered) {
|
|
EXPECT_TRUE(ClassRegistryDatabase::Instance()->Find("TCPStreamJoin") !=
|
|
NULL_PTR(const ClassRegistryItem *));
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, JoinMergesConfigAndHoldsSlowValues) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream fast("Svc", "fast");
|
|
TCPSServiceStream slow("Svc", "slow");
|
|
ASSERT_TRUE(StartStream(fast));
|
|
ASSERT_TRUE(StartStream(slow));
|
|
|
|
std::vector<SigSpec> fs, ss;
|
|
SigSpec a = {"t", UDPS_NO_TIME_SIGNAL}, b = {"x", 0u};
|
|
fs.push_back(a);
|
|
fs.push_back(b);
|
|
SigSpec c = {"time", UDPS_NO_TIME_SIGNAL}, d = {"y", 0u};
|
|
ss.push_back(c);
|
|
ss.push_back(d);
|
|
std::vector<uint8> fcfg = MakeConfig(fs, UDPS_PUBLISH_STRICT, kHrt);
|
|
std::vector<uint8> scfg = MakeConfig(ss, UDPS_PUBLISH_DECIMATE, kHrt);
|
|
ASSERT_TRUE(fast.SendConfig(&fcfg[0], static_cast<uint32>(fcfg.size())));
|
|
ASSERT_TRUE(slow.SendConfig(&scfg[0], static_cast<uint32>(scfg.size())));
|
|
|
|
bool ok = false;
|
|
ReferenceT<TCPStreamJoin> join = MakeJoin(
|
|
"Class = TCPStreamJoin Service = Svc StreamName = all Sources = \"fast, slow\" Master = fast", ok);
|
|
ASSERT_TRUE(ok);
|
|
EXPECT_TRUE(join->IsActive());
|
|
EXPECT_TRUE(join->HasConfig());
|
|
EXPECT_EQ(2u, join->GetNumSources());
|
|
// A tapped source must keep publishing even without a socket subscriber.
|
|
EXPECT_TRUE(fast.HasClients());
|
|
EXPECT_TRUE(slow.HasClients());
|
|
EXPECT_EQ(3u, fx.svc->GetStreamCount());
|
|
|
|
int fd = Subscribe(fx.Port(), "all");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitSvcClients(fx.svc.operator->(), 1u));
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
ASSERT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
ASSERT_EQ(4u + 4u * UDPS_SIGNAL_DESC_SIZE + 1u + 8u, f.payload.size());
|
|
uint32 n = 0u;
|
|
memcpy(&n, &f.payload[0], 4u);
|
|
EXPECT_EQ(4u, n);
|
|
const char *const names[] = {"fast_t", "fast_x", "slow_time", "slow_y"};
|
|
const uint32 timeIdx[] = {UDPS_NO_TIME_SIGNAL, 0u, UDPS_NO_TIME_SIGNAL, 2u};
|
|
for (uint32 i = 0u; i < 4u; i++) {
|
|
UDPSSignalDescriptor dd;
|
|
memcpy(&dd, &f.payload[4u + i * UDPS_SIGNAL_DESC_SIZE], UDPS_SIGNAL_DESC_SIZE);
|
|
EXPECT_STREQ(names[i], dd.name);
|
|
EXPECT_EQ(timeIdx[i], dd.timeSignalIdx) << names[i];
|
|
}
|
|
const size_t tail = 4u + 4u * UDPS_SIGNAL_DESC_SIZE;
|
|
EXPECT_EQ(UDPS_PUBLISH_STRICT, f.payload[tail]);
|
|
uint64 hrt = 0u;
|
|
memcpy(&hrt, &f.payload[tail + 1u], 8u);
|
|
EXPECT_EQ(kHrt, hrt);
|
|
|
|
// The master alone cannot emit until the slow source has delivered one frame.
|
|
ASSERT_TRUE(fast.SendData(1u, &MakeData(100u, Payload(8u, 1u))[0], 16u));
|
|
EXPECT_EQ(0u, join->GetFramesEmitted());
|
|
|
|
ASSERT_TRUE(slow.SendData(1u, &MakeData(555u, Payload(4u, 0x40u))[0], 12u));
|
|
EXPECT_EQ(0u, join->GetFramesEmitted()); // the slow source never paces the output
|
|
|
|
ASSERT_TRUE(fast.SendData(2u, &MakeData(200u, Payload(8u, 2u))[0], 16u));
|
|
ASSERT_TRUE(fast.SendData(3u, &MakeData(300u, Payload(8u, 3u))[0], 16u));
|
|
ASSERT_TRUE(slow.SendData(2u, &MakeData(777u, Payload(4u, 0x50u))[0], 12u));
|
|
ASSERT_TRUE(fast.SendData(4u, &MakeData(400u, Payload(8u, 4u))[0], 16u));
|
|
EXPECT_EQ(3u, join->GetFramesEmitted());
|
|
|
|
const uint64 ts[] = {200u, 300u, 400u};
|
|
const uint8 fseed[] = {2u, 3u, 4u};
|
|
const uint8 sseed[] = {0x40u, 0x40u, 0x50u};
|
|
for (uint32 i = 0u; i < 3u; i++) {
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
EXPECT_EQ(UDPS_TYPE_DATA, f.type);
|
|
EXPECT_EQ(i + 1u, f.counter);
|
|
ASSERT_EQ(8u + 8u + 4u, f.payload.size());
|
|
uint64 got = 0u;
|
|
memcpy(&got, &f.payload[0], 8u);
|
|
EXPECT_EQ(ts[i], got);
|
|
EXPECT_EQ(Payload(8u, fseed[i]), std::vector<uint8>(f.payload.begin() + 8, f.payload.begin() + 16));
|
|
EXPECT_EQ(Payload(4u, sseed[i]), std::vector<uint8>(f.payload.begin() + 16, f.payload.end()));
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, JoinWaitsForSourcesAndFollowsTheirLifecycle) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream fast("Svc", "fast");
|
|
ASSERT_TRUE(StartStream(fast));
|
|
std::vector<SigSpec> fs;
|
|
SigSpec a = {"v", UDPS_NO_TIME_SIGNAL};
|
|
fs.push_back(a);
|
|
std::vector<uint8> cfg = MakeConfig(fs, UDPS_PUBLISH_STRICT, kHrt);
|
|
ASSERT_TRUE(fast.SendConfig(&cfg[0], static_cast<uint32>(cfg.size())));
|
|
|
|
bool ok = false;
|
|
ReferenceT<TCPStreamJoin> join = MakeJoin(
|
|
"Class = TCPStreamJoin Service = Svc StreamName = all Sources = \"fast,slow\"", ok);
|
|
ASSERT_TRUE(ok);
|
|
EXPECT_FALSE(join->IsActive());
|
|
EXPECT_EQ(1u, fx.svc->GetStreamCount()); // merged stream not registered yet
|
|
|
|
TCPSServiceStream *slow = new TCPSServiceStream("Svc", "slow");
|
|
ASSERT_TRUE(StartStream(*slow));
|
|
EXPECT_TRUE(join->IsActive());
|
|
EXPECT_FALSE(join->HasConfig()); // slow has not published a CONFIG yet
|
|
EXPECT_EQ(3u, fx.svc->GetStreamCount());
|
|
|
|
ASSERT_TRUE(slow->SendConfig(&cfg[0], static_cast<uint32>(cfg.size())));
|
|
EXPECT_TRUE(join->HasConfig());
|
|
|
|
int fd = Subscribe(fx.Port(), "all");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitSvcClients(fx.svc.operator->(), 1u));
|
|
Frame f;
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
|
|
// Losing a source removes the merged stream and drops its subscribers.
|
|
delete slow;
|
|
EXPECT_FALSE(join->IsActive());
|
|
EXPECT_FALSE(join->HasConfig());
|
|
EXPECT_EQ(1u, fx.svc->GetStreamCount());
|
|
EXPECT_TRUE(PeerClosed(fd, 3000));
|
|
close(fd);
|
|
|
|
// The source returns: the merged stream comes back once its CONFIG is known.
|
|
slow = new TCPSServiceStream("Svc", "slow");
|
|
ASSERT_TRUE(StartStream(*slow));
|
|
EXPECT_TRUE(join->IsActive());
|
|
ASSERT_TRUE(slow->SendConfig(&cfg[0], static_cast<uint32>(cfg.size())));
|
|
EXPECT_TRUE(join->HasConfig());
|
|
EXPECT_EQ(3u, fx.svc->GetStreamCount());
|
|
|
|
// A CONFIG change of a source republishes the merged CONFIG.
|
|
fd = Subscribe(fx.Port(), "all");
|
|
ASSERT_GE(fd, 0);
|
|
ASSERT_TRUE(WaitSvcClients(fx.svc.operator->(), 1u));
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
EXPECT_EQ(4u + 2u * UDPS_SIGNAL_DESC_SIZE + 1u + 8u, f.payload.size());
|
|
fs.push_back(a);
|
|
fs[1].name = "w";
|
|
std::vector<uint8> cfg2 = MakeConfig(fs, UDPS_PUBLISH_STRICT, kHrt);
|
|
ASSERT_TRUE(slow->SendConfig(&cfg2[0], static_cast<uint32>(cfg2.size())));
|
|
ASSERT_TRUE(ReadFrame(fd, f, 3000));
|
|
EXPECT_EQ(UDPS_TYPE_CONFIG, f.type);
|
|
EXPECT_EQ(4u + 3u * UDPS_SIGNAL_DESC_SIZE + 1u + 8u, f.payload.size());
|
|
close(fd);
|
|
delete slow;
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, JoinRefusesUnmergeableSources) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
TCPSServiceStream a("Svc", "a");
|
|
TCPSServiceStream b("Svc", "b");
|
|
ASSERT_TRUE(StartStream(a));
|
|
ASSERT_TRUE(StartStream(b));
|
|
std::vector<SigSpec> s;
|
|
SigSpec v = {"v", UDPS_NO_TIME_SIGNAL};
|
|
s.push_back(v);
|
|
|
|
bool ok = false;
|
|
// Accumulate source: the merged CONFIG is never published.
|
|
{
|
|
std::vector<uint8> ca = MakeConfig(s, UDPS_PUBLISH_ACCUMULATE, kHrt);
|
|
std::vector<uint8> cb = MakeConfig(s, UDPS_PUBLISH_STRICT, kHrt);
|
|
ASSERT_TRUE(a.SendConfig(&ca[0], static_cast<uint32>(ca.size())));
|
|
ASSERT_TRUE(b.SendConfig(&cb[0], static_cast<uint32>(cb.size())));
|
|
ReferenceT<TCPStreamJoin> j = MakeJoin(
|
|
"Class = TCPStreamJoin Service = Svc StreamName = m Sources = \"a,b\"", ok);
|
|
ASSERT_TRUE(ok);
|
|
EXPECT_TRUE(j->IsActive());
|
|
EXPECT_FALSE(j->HasConfig());
|
|
}
|
|
// Different HRT frequencies.
|
|
{
|
|
std::vector<uint8> ca = MakeConfig(s, UDPS_PUBLISH_STRICT, kHrt);
|
|
std::vector<uint8> cb = MakeConfig(s, UDPS_PUBLISH_STRICT, kHrt / 2u);
|
|
ASSERT_TRUE(a.SendConfig(&ca[0], static_cast<uint32>(ca.size())));
|
|
ASSERT_TRUE(b.SendConfig(&cb[0], static_cast<uint32>(cb.size())));
|
|
ReferenceT<TCPStreamJoin> j = MakeJoin(
|
|
"Class = TCPStreamJoin Service = Svc StreamName = m Sources = \"a,b\"", ok);
|
|
ASSERT_TRUE(ok);
|
|
EXPECT_FALSE(j->HasConfig());
|
|
}
|
|
// An unknown HRT frequency matches any.
|
|
{
|
|
std::vector<uint8> ca = MakeConfig(s, UDPS_PUBLISH_STRICT, 0u, false);
|
|
std::vector<uint8> cb = MakeConfig(s, UDPS_PUBLISH_STRICT, kHrt);
|
|
ASSERT_TRUE(a.SendConfig(&ca[0], static_cast<uint32>(ca.size())));
|
|
ASSERT_TRUE(b.SendConfig(&cb[0], static_cast<uint32>(cb.size())));
|
|
ReferenceT<TCPStreamJoin> j = MakeJoin(
|
|
"Class = TCPStreamJoin Service = Svc StreamName = m Sources = \"a,b\"", ok);
|
|
ASSERT_TRUE(ok);
|
|
EXPECT_TRUE(j->HasConfig());
|
|
}
|
|
// A merged signal name that does not fit 63 characters.
|
|
{
|
|
std::string longName(62u, 'q');
|
|
std::vector<SigSpec> ls;
|
|
SigSpec l = {longName.c_str(), UDPS_NO_TIME_SIGNAL};
|
|
ls.push_back(l);
|
|
std::vector<uint8> ca = MakeConfig(ls, UDPS_PUBLISH_STRICT, kHrt);
|
|
std::vector<uint8> cb = MakeConfig(s, UDPS_PUBLISH_STRICT, kHrt);
|
|
ASSERT_TRUE(a.SendConfig(&ca[0], static_cast<uint32>(ca.size())));
|
|
ASSERT_TRUE(b.SendConfig(&cb[0], static_cast<uint32>(cb.size())));
|
|
ReferenceT<TCPStreamJoin> j = MakeJoin(
|
|
"Class = TCPStreamJoin Service = Svc StreamName = m Sources = \"a,b\"", ok);
|
|
ASSERT_TRUE(ok);
|
|
EXPECT_FALSE(j->HasConfig());
|
|
}
|
|
}
|
|
|
|
TEST(TCPStreamServiceGTest, JoinRejectsInvalidParameters) {
|
|
ServiceFixture fx;
|
|
ASSERT_TRUE(fx.ok);
|
|
const char *const bad[] = {
|
|
"Class = TCPStreamJoin StreamName = m Sources = \"a,b\"", // no Service
|
|
"Class = TCPStreamJoin Service = Svc StreamName = m", // no Sources
|
|
"Class = TCPStreamJoin Service = Nope StreamName = m Sources = \"a,b\"", // unknown service
|
|
"Class = TCPStreamJoin Service = Svc StreamName = m Sources = \"a,a\"", // duplicate
|
|
"Class = TCPStreamJoin Service = Svc StreamName = m Sources = \"a,m\"", // own output
|
|
"Class = TCPStreamJoin Service = Svc StreamName = m Sources = \"a,,b\"", // empty name
|
|
"Class = TCPStreamJoin Service = Svc StreamName = m Sources = \"a,b\" Master = c", // bad master
|
|
};
|
|
for (size_t i = 0u; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
|
bool ok = true;
|
|
ReferenceT<TCPStreamJoin> j = MakeJoin(bad[i], ok);
|
|
EXPECT_FALSE(ok) << bad[i];
|
|
}
|
|
}
|