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>
This commit is contained in:
1 parent
5562877c99
commit
50d85363a3
79 files changed
+12348
-138
No files matched your search
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* @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());
|
||||
}
|
||||
Reference in new issue
Block a user