Files
Martino Ferrari 0ae35d11ff feat(streamhub): BinaryRecorder core (header, encode, rows, flush, rotation)
Add a FileWriter-compatible per-source binary recorder: native-type encode
and TypeDescriptor mapping, FileWriter header serialization, subset/quantized/
ACCUMULATE row serialization, and push-thread double-buffer flush with size-cap
rotation, keep-N pruning, fdatasync cadence, disk-free and staging-overflow
guards. Covered by 12 GTests; full suite (83) green.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-25 01:05:32 +02:00

440 lines
15 KiB
C++

/**
* @file BinaryRecorderGTest.cpp
* @brief Unit tests for the StreamHub BinaryRecorder.
*
* @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and
* the Development of Fusion Energy ('Fusion for Energy').
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved
* by the European Commission - subsequent versions of the EUPL (the "Licence")
* You may not use this work except in compliance with the Licence.
* You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
*
* @warning Unless required by applicable law or agreed to in writing,
* software distributed under the Licence is distributed on an "AS IS"
* basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the Licence permissions and limitations under the Licence.
*/
#include "BinaryRecorder.h"
#include "TypeDescriptor.h"
#include "UDPSProtocol.h"
#include <gtest/gtest.h>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <vector>
#include <string>
#include <dirent.h>
#include <unistd.h>
using StreamHub::BinaryRecorder;
using StreamHub::RecorderConfig;
using MARTe::uint8;
using MARTe::uint16;
using MARTe::uint32;
using MARTe::uint64;
using MARTe::float64;
/*---------------------------------------------------------------------------*/
/* Helpers */
/*---------------------------------------------------------------------------*/
static MARTe::UDPSSignalDescriptor MakeDesc(const char *name, uint8 tc,
uint32 nElems, uint8 quant = 0u,
double rmin = 0.0, double rmax = 0.0) {
MARTe::UDPSSignalDescriptor d;
memset(&d, 0, sizeof(d));
strncpy(d.name, name, MARTe::UDPS_MAX_SIGNAL_NAME - 1u);
d.typeCode = tc;
d.quantType = quant;
d.numDimensions = (nElems > 1u) ? 1u : 0u;
d.numRows = nElems;
d.numCols = 1u;
d.rangeMin = rmin;
d.rangeMax = rmax;
d.timeMode = MARTe::UDPS_TIMEMODE_PACKET;
d.timeSignalIdx = MARTe::UDPS_NO_TIME_SIGNAL;
return d;
}
static RecorderConfig MakeCfg(const char *dir) {
RecorderConfig c;
memset(&c, 0, sizeof(c));
c.enabled = true;
c.autoStart = true;
strncpy(c.directory, dir, sizeof(c.directory) - 1u);
c.maxFileBytes = 1024u * 1024u;
c.keepFiles = 8u;
c.stagingBytes = 1024u * 1024u;
c.flushIntervalSec = 5u;
c.minDiskFreeMB = 0u;
strncpy(c.signals, "all", sizeof(c.signals) - 1u);
return c;
}
static std::string MakeTempDir() {
char templ[] = "/tmp/streamhub_rec_XXXXXX";
char *p = mkdtemp(templ);
return std::string(p != NULL ? p : "/tmp");
}
static std::vector<uint8> ReadFileBytes(const std::string &path) {
std::vector<uint8> out;
FILE *f = fopen(path.c_str(), "rb");
if (f == NULL) { return out; }
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
if (n > 0) {
out.resize(static_cast<size_t>(n));
size_t rd = fread(&out[0], 1u, static_cast<size_t>(n), f);
out.resize(rd);
}
fclose(f);
return out;
}
static std::vector<std::string> ListBinFiles(const std::string &dir) {
std::vector<std::string> out;
DIR *d = opendir(dir.c_str());
if (d == NULL) { return out; }
struct dirent *e;
while ((e = readdir(d)) != NULL) {
std::string nm = e->d_name;
if (nm.size() > 4u && nm.substr(nm.size() - 4u) == ".bin") {
out.push_back(dir + "/" + nm);
}
}
closedir(d);
return out;
}
static uint16 ReadU16(const std::vector<uint8> &b, size_t off) {
uint16 v = 0u; memcpy(&v, &b[off], 2u); return v;
}
static uint32 ReadU32(const std::vector<uint8> &b, size_t off) {
uint32 v = 0u; memcpy(&v, &b[off], 4u); return v;
}
/*---------------------------------------------------------------------------*/
/* Static helper tests */
/*---------------------------------------------------------------------------*/
TEST(BinaryRecorderGTest, TypeCodeToDescriptorAll) {
EXPECT_EQ(MARTe::UnsignedInteger32Bit.all,
BinaryRecorder::TypeCodeToDescriptorAll(MARTe::UDPS_TYPECODE_UINT32));
EXPECT_EQ(MARTe::Float32Bit.all,
BinaryRecorder::TypeCodeToDescriptorAll(MARTe::UDPS_TYPECODE_FLOAT32));
EXPECT_EQ(MARTe::Float64Bit.all,
BinaryRecorder::TypeCodeToDescriptorAll(MARTe::UDPS_TYPECODE_FLOAT64));
EXPECT_EQ(MARTe::UnsignedInteger64Bit.all,
BinaryRecorder::TypeCodeToDescriptorAll(MARTe::UDPS_TYPECODE_UINT64));
EXPECT_EQ(MARTe::SignedInteger16Bit.all,
BinaryRecorder::TypeCodeToDescriptorAll(MARTe::UDPS_TYPECODE_INT16));
}
TEST(BinaryRecorderGTest, EncodeNativeFloat32) {
uint8 buf[8] = {0};
uint32 n = BinaryRecorder::EncodeNative(MARTe::UDPS_TYPECODE_FLOAT32, 1.5, buf);
ASSERT_EQ(4u, n);
float expect = 1.5f;
EXPECT_EQ(0, memcmp(buf, &expect, 4u));
}
TEST(BinaryRecorderGTest, EncodeNativeUint32) {
uint8 buf[8] = {0};
uint32 n = BinaryRecorder::EncodeNative(MARTe::UDPS_TYPECODE_UINT32, 7.0, buf);
ASSERT_EQ(4u, n);
uint32 expect = 7u;
EXPECT_EQ(0, memcmp(buf, &expect, 4u));
}
TEST(BinaryRecorderGTest, EncodeNativeInt16) {
uint8 buf[8] = {0};
uint32 n = BinaryRecorder::EncodeNative(MARTe::UDPS_TYPECODE_INT16, -3.0, buf);
ASSERT_EQ(2u, n);
MARTe::int16 expect = -3;
EXPECT_EQ(0, memcmp(buf, &expect, 2u));
}
/*---------------------------------------------------------------------------*/
/* Header bytes */
/*---------------------------------------------------------------------------*/
TEST(BinaryRecorderGTest, HeaderBytes) {
std::string dir = MakeTempDir();
RecorderConfig cfg = MakeCfg(dir.c_str());
BinaryRecorder rec;
rec.Init(cfg, "S0");
MARTe::UDPSSignalDescriptor descs[2] = {
MakeDesc("Sine", MARTe::UDPS_TYPECODE_FLOAT32, 1u),
MakeDesc("Time", MARTe::UDPS_TYPECODE_UINT32, 4u)
};
bool mask[2] = { true, true };
rec.Configure(descs, 2u, mask);
rec.FlushTick(0u);
bool recording; char path[768]; uint64 bw, rw, dr, fm;
rec.GetInfo(recording, path, sizeof(path), bw, rw, dr, fm);
std::vector<uint8> b = ReadFileBytes(path);
ASSERT_GE(b.size(), 4u + 2u * 38u);
EXPECT_EQ(2u, ReadU32(b, 0));
/* descriptor 0 */
EXPECT_EQ(MARTe::Float32Bit.all, ReadU16(b, 4));
EXPECT_STREQ("Sine", reinterpret_cast<const char *>(&b[6]));
EXPECT_EQ(1u, ReadU32(b, 38));
/* descriptor 1 */
EXPECT_EQ(MARTe::UnsignedInteger32Bit.all, ReadU16(b, 42));
EXPECT_STREQ("Time", reinterpret_cast<const char *>(&b[44]));
EXPECT_EQ(4u, ReadU32(b, 76));
}
/*---------------------------------------------------------------------------*/
/* Row serialization */
/*---------------------------------------------------------------------------*/
TEST(BinaryRecorderGTest, RowStrictUnquantized) {
std::string dir = MakeTempDir();
BinaryRecorder rec;
rec.Init(MakeCfg(dir.c_str()), "S0");
MARTe::UDPSSignalDescriptor descs[2] = {
MakeDesc("A", MARTe::UDPS_TYPECODE_FLOAT32, 1u),
MakeDesc("B", MARTe::UDPS_TYPECODE_UINT32, 2u)
};
bool mask[2] = { true, true };
rec.Configure(descs, 2u, mask);
uint8 payload[12];
float a = 2.5f; uint32 b0 = 10u, b1 = 20u;
memcpy(payload + 0, &a, 4u);
memcpy(payload + 4, &b0, 4u);
memcpy(payload + 8, &b1, 4u);
uint32 sigOff[2] = { 0u, 4u };
uint32 sigElems[2] = { 1u, 2u };
rec.CapturePacket(payload, sigOff, sigElems, MARTe::UDPS_PUBLISH_STRICT, 1u);
rec.FlushTick(0u);
bool recording; char path[768]; uint64 bw, rw, dr, fm;
rec.GetInfo(recording, path, sizeof(path), bw, rw, dr, fm);
std::vector<uint8> b = ReadFileBytes(path);
const size_t hdr = 4u + 2u * 38u;
ASSERT_EQ(hdr + 12u, b.size());
float ra; uint32 rb0, rb1;
memcpy(&ra, &b[hdr + 0], 4u);
memcpy(&rb0, &b[hdr + 4], 4u);
memcpy(&rb1, &b[hdr + 8], 4u);
EXPECT_FLOAT_EQ(2.5f, ra);
EXPECT_EQ(10u, rb0);
EXPECT_EQ(20u, rb1);
EXPECT_EQ(1u, rw);
}
TEST(BinaryRecorderGTest, RowAccumulateExpansion) {
std::string dir = MakeTempDir();
BinaryRecorder rec;
rec.Init(MakeCfg(dir.c_str()), "S0");
MARTe::UDPSSignalDescriptor descs[2] = {
MakeDesc("S", MARTe::UDPS_TYPECODE_FLOAT32, 1u),
MakeDesc("C", MARTe::UDPS_TYPECODE_UINT32, 1u)
};
bool mask[2] = { true, true };
rec.Configure(descs, 2u, mask);
/* Wire: S accumulated x3 (3 floats), C scalar (1 uint32). */
uint8 payload[16];
float s[3] = { 1.0f, 2.0f, 3.0f };
uint32 c = 9u;
memcpy(payload + 0, s, 12u);
memcpy(payload + 12, &c, 4u);
uint32 sigOff[2] = { 0u, 12u };
uint32 sigElems[2] = { 3u, 1u };
rec.CapturePacket(payload, sigOff, sigElems, MARTe::UDPS_PUBLISH_ACCUMULATE, 3u);
rec.FlushTick(0u);
bool recording; char path[768]; uint64 bw, rw, dr, fm;
rec.GetInfo(recording, path, sizeof(path), bw, rw, dr, fm);
std::vector<uint8> b = ReadFileBytes(path);
const size_t hdr = 4u + 2u * 38u;
const size_t rowBytes = 8u;
ASSERT_EQ(hdr + 3u * rowBytes, b.size());
for (uint32 r = 0u; r < 3u; r++) {
float sr; uint32 cr;
memcpy(&sr, &b[hdr + r * rowBytes + 0], 4u);
memcpy(&cr, &b[hdr + r * rowBytes + 4], 4u);
EXPECT_FLOAT_EQ(static_cast<float>(r + 1u), sr);
EXPECT_EQ(9u, cr);
}
EXPECT_EQ(3u, rw);
}
TEST(BinaryRecorderGTest, SubsetSelection) {
std::string dir = MakeTempDir();
BinaryRecorder rec;
rec.Init(MakeCfg(dir.c_str()), "S0");
MARTe::UDPSSignalDescriptor descs[3] = {
MakeDesc("A", MARTe::UDPS_TYPECODE_FLOAT32, 1u),
MakeDesc("B", MARTe::UDPS_TYPECODE_FLOAT32, 1u),
MakeDesc("C", MARTe::UDPS_TYPECODE_UINT32, 1u)
};
bool mask[3] = { true, false, true };
rec.Configure(descs, 3u, mask);
uint8 payload[12];
float a = 1.0f, bb = 2.0f; uint32 c = 3u;
memcpy(payload + 0, &a, 4u);
memcpy(payload + 4, &bb, 4u);
memcpy(payload + 8, &c, 4u);
uint32 sigOff[3] = { 0u, 4u, 8u };
uint32 sigElems[3] = { 1u, 1u, 1u };
rec.CapturePacket(payload, sigOff, sigElems, MARTe::UDPS_PUBLISH_STRICT, 1u);
rec.FlushTick(0u);
bool recording; char path[768]; uint64 bw, rw, dr, fm;
rec.GetInfo(recording, path, sizeof(path), bw, rw, dr, fm);
std::vector<uint8> b = ReadFileBytes(path);
/* header has 2 signals, A and C */
EXPECT_EQ(2u, ReadU32(b, 0));
EXPECT_STREQ("A", reinterpret_cast<const char *>(&b[6]));
EXPECT_STREQ("C", reinterpret_cast<const char *>(&b[44]));
const size_t hdr = 4u + 2u * 38u;
ASSERT_EQ(hdr + 8u, b.size());
float ra; uint32 rc;
memcpy(&ra, &b[hdr + 0], 4u);
memcpy(&rc, &b[hdr + 4], 4u);
EXPECT_FLOAT_EQ(1.0f, ra);
EXPECT_EQ(3u, rc);
}
TEST(BinaryRecorderGTest, QuantizedReconstruction) {
std::string dir = MakeTempDir();
BinaryRecorder rec;
rec.Init(MakeCfg(dir.c_str()), "S0");
/* float32 signal quantized to uint16 over [0,10]. */
MARTe::UDPSSignalDescriptor descs[1] = {
MakeDesc("Q", MARTe::UDPS_TYPECODE_FLOAT32, 1u,
MARTe::UDPS_QUANT_UINT16, 0.0, 10.0)
};
bool mask[1] = { true };
rec.Configure(descs, 1u, mask);
uint8 payload[2];
uint16 q = 32767u;
memcpy(payload, &q, 2u);
uint32 sigOff[1] = { 0u };
uint32 sigElems[1] = { 1u };
rec.CapturePacket(payload, sigOff, sigElems, MARTe::UDPS_PUBLISH_STRICT, 1u);
rec.FlushTick(0u);
bool recording; char path[768]; uint64 bw, rw, dr, fm;
rec.GetInfo(recording, path, sizeof(path), bw, rw, dr, fm);
std::vector<uint8> b = ReadFileBytes(path);
const size_t hdr = 4u + 38u;
ASSERT_EQ(hdr + 4u, b.size());
float rv;
memcpy(&rv, &b[hdr], 4u);
double expect = 0.0 + (32767.0 / 65535.0) * 10.0;
EXPECT_NEAR(expect, static_cast<double>(rv), 1e-3);
}
/*---------------------------------------------------------------------------*/
/* Flush / rotation / guards */
/*---------------------------------------------------------------------------*/
TEST(BinaryRecorderGTest, RotationKeepN) {
std::string dir = MakeTempDir();
RecorderConfig cfg = MakeCfg(dir.c_str());
cfg.maxFileBytes = 200u;
cfg.keepFiles = 2u;
BinaryRecorder rec;
rec.Init(cfg, "S0");
MARTe::UDPSSignalDescriptor descs[2] = {
MakeDesc("A", MARTe::UDPS_TYPECODE_FLOAT32, 1u),
MakeDesc("B", MARTe::UDPS_TYPECODE_UINT32, 2u)
};
bool mask[2] = { true, true };
rec.Configure(descs, 2u, mask);
uint8 payload[12];
float a = 1.0f; uint32 b0 = 1u, b1 = 2u;
memcpy(payload + 0, &a, 4u);
memcpy(payload + 4, &b0, 4u);
memcpy(payload + 8, &b1, 4u);
uint32 sigOff[2] = { 0u, 4u };
uint32 sigElems[2] = { 1u, 2u };
for (uint32 i = 0u; i < 50u; i++) {
rec.CapturePacket(payload, sigOff, sigElems, MARTe::UDPS_PUBLISH_STRICT, 1u);
rec.FlushTick(0u);
}
std::vector<std::string> files = ListBinFiles(dir);
EXPECT_LE(files.size(), 2u);
EXPECT_GE(files.size(), 1u);
for (size_t i = 0u; i < files.size(); i++) {
std::vector<uint8> b = ReadFileBytes(files[i]);
ASSERT_GE(b.size(), 4u);
EXPECT_EQ(2u, ReadU32(b, 0));
}
}
TEST(BinaryRecorderGTest, DiskGuardStops) {
std::string dir = MakeTempDir();
RecorderConfig cfg = MakeCfg(dir.c_str());
cfg.minDiskFreeMB = 0xFFFFFFFFu; /* impossibly large → always low */
BinaryRecorder rec;
rec.Init(cfg, "S0");
MARTe::UDPSSignalDescriptor descs[1] = {
MakeDesc("A", MARTe::UDPS_TYPECODE_FLOAT32, 1u)
};
bool mask[1] = { true };
rec.Configure(descs, 1u, mask);
rec.FlushTick(0u);
bool recording; char path[768]; uint64 bw, rw, dr, fm;
rec.GetInfo(recording, path, sizeof(path), bw, rw, dr, fm);
EXPECT_FALSE(recording);
EXPECT_EQ(0u, ListBinFiles(dir).size());
}
TEST(BinaryRecorderGTest, OverflowDropsRows) {
std::string dir = MakeTempDir();
RecorderConfig cfg = MakeCfg(dir.c_str());
cfg.stagingBytes = 4096u; /* small staging, no flush → overflow */
BinaryRecorder rec;
rec.Init(cfg, "S0");
MARTe::UDPSSignalDescriptor descs[2] = {
MakeDesc("A", MARTe::UDPS_TYPECODE_FLOAT32, 1u),
MakeDesc("B", MARTe::UDPS_TYPECODE_UINT32, 2u)
};
bool mask[2] = { true, true };
rec.Configure(descs, 2u, mask);
uint8 payload[12];
float a = 1.0f; uint32 b0 = 1u, b1 = 2u;
memcpy(payload + 0, &a, 4u);
memcpy(payload + 4, &b0, 4u);
memcpy(payload + 8, &b1, 4u);
uint32 sigOff[2] = { 0u, 4u };
uint32 sigElems[2] = { 1u, 2u };
/* Capture far more rows than the 4096-byte buffer can hold (no flush). */
for (uint32 i = 0u; i < 500u; i++) {
rec.CapturePacket(payload, sigOff, sigElems, MARTe::UDPS_PUBLISH_STRICT, 1u);
}
bool recording; char path[768]; uint64 bw, rw, dr, fm;
rec.GetInfo(recording, path, sizeof(path), bw, rw, dr, fm);
EXPECT_GT(dr, 0u);
}