136 lines
5.2 KiB
C++
136 lines
5.2 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include <clocale>
|
|
|
|
#include "fgc/ChannelHistory.h"
|
|
#include "fgc/ChannelTopics.h"
|
|
|
|
#include <string>
|
|
|
|
using namespace fgc;
|
|
|
|
TEST_CASE("ChannelHistory counts each delivery outcome separately") {
|
|
ChannelHistory h;
|
|
h.record(true, Delivery::Ok, "t/a", "1");
|
|
h.record(true, Delivery::Ok, "t/a", "2");
|
|
h.record(true, Delivery::Dropped, "t/a", "3");
|
|
h.record(true, Delivery::Failed, "t/a", "4");
|
|
h.record(false, Delivery::Ok, "t/b", "in");
|
|
|
|
ChannelStats s = h.stats();
|
|
CHECK(s.published_ok == 2);
|
|
CHECK(s.published_dropped == 1); // the silent-drop path: the number that matters
|
|
CHECK(s.published_failed == 1);
|
|
CHECK(s.received == 1); // inbound never touches the publish counters
|
|
}
|
|
|
|
TEST_CASE("ChannelHistory returns messages oldest first and keeps their fields") {
|
|
ChannelHistory h;
|
|
h.record(true, Delivery::Dropped, "t/first", "a");
|
|
h.record(false, Delivery::Ok, "t/second", "b");
|
|
|
|
auto r = h.recent();
|
|
REQUIRE(r.size() == 2);
|
|
CHECK(r[0].topic == "t/first");
|
|
CHECK(r[0].outbound);
|
|
CHECK(r[0].delivery == Delivery::Dropped);
|
|
CHECK(r[0].payload == "a");
|
|
CHECK(r[1].topic == "t/second");
|
|
CHECK_FALSE(r[1].outbound);
|
|
CHECK(r[1].timestamp_ms > 0);
|
|
}
|
|
|
|
TEST_CASE("ChannelHistory evicts the oldest beyond its capacity") {
|
|
ChannelHistory h;
|
|
const int n = static_cast<int>(ChannelHistory::kCapacity) + 10;
|
|
for (int i = 0; i < n; ++i) h.record(true, Delivery::Ok, "t", std::to_string(i));
|
|
|
|
auto r = h.recent();
|
|
REQUIRE(r.size() == ChannelHistory::kCapacity);
|
|
CHECK(r.front().payload == std::to_string(n - static_cast<int>(ChannelHistory::kCapacity)));
|
|
CHECK(r.back().payload == std::to_string(n - 1));
|
|
// Counters are cumulative; they must not be bounded by the ring.
|
|
CHECK(h.stats().published_ok == n);
|
|
}
|
|
|
|
TEST_CASE("ChannelHistory tracks connects, losses and the last status sent") {
|
|
ChannelHistory h;
|
|
CHECK(h.stats().connects == 0);
|
|
CHECK(h.stats().last_connect_ms == 0);
|
|
|
|
h.noteConnect();
|
|
h.noteLoss();
|
|
h.noteConnect();
|
|
h.noteStatus("1");
|
|
|
|
ChannelStats s = h.stats();
|
|
CHECK(s.connects == 2);
|
|
CHECK(s.last_connect_ms > 0);
|
|
CHECK(s.last_loss_ms > 0);
|
|
CHECK(s.last_status_payload == "1");
|
|
}
|
|
|
|
TEST_CASE("topic and payload builders match the documented wire format") {
|
|
CHECK(towerTopic("Tower", kTopicCamEvent) == "GGS/FWT/Tower/CamEvent");
|
|
|
|
CamEvent c;
|
|
c.tower = "Tower"; c.camera = "RGB";
|
|
c.heading_decideg = 1373; c.pitch_decideg = 300; c.timestamp_ms = 1719312345678LL;
|
|
const std::string j = camEventJson(c);
|
|
CHECK(j.find("\"fwt\":\"Tower\"") != std::string::npos);
|
|
CHECK(j.find("\"cam\":\"RGB\"") != std::string::npos);
|
|
CHECK(j.find("\"hdg\":1373") != std::string::npos);
|
|
CHECK(j.find("\"pit\":300") != std::string::npos);
|
|
CHECK(j.find("\"time\":1719312345678") != std::string::npos);
|
|
// The quality gate is gone; nothing should still be publishing a verdict.
|
|
CHECK(j.find("degraded") == std::string::npos);
|
|
// A colour camera's event carries none of the thermal fields.
|
|
CHECK(j.find("\"cal\"") == std::string::npos);
|
|
CHECK(j == "{ \"fwt\":\"Tower\", \"cam\":\"RGB\", \"hdg\":1373, \"pit\":300, "
|
|
"\"time\":1719312345678 }");
|
|
}
|
|
|
|
TEST_CASE("a thermal CamEvent carries profile, packets, temperatures and hot spot") {
|
|
CamEvent c;
|
|
c.tower = "Tower"; c.camera = "TIR";
|
|
c.heading_decideg = 1234; c.pitch_decideg = -30; c.timestamp_ms = 1791234567890LL;
|
|
ThermalEventFields t;
|
|
t.cal = 0; t.missed = 2; t.stats_ok = true;
|
|
t.tmax_decic = 3015; t.tmed_decic = 187; t.hotx = 0.73594; t.hoty = 0.5833;
|
|
c.thermal = t;
|
|
CHECK(camEventJson(c) ==
|
|
"{ \"fwt\":\"Tower\", \"cam\":\"TIR\", \"hdg\":1234, \"pit\":-30, "
|
|
"\"time\":1791234567890, \"cal\":0, \"missed\":2, \"tmax\":3015, \"tmed\":187, "
|
|
"\"hotx\":0.7359, \"hoty\":0.5833 }");
|
|
|
|
// Without valid pixels the profile and packets still go out, the rest as null.
|
|
c.thermal->stats_ok = false;
|
|
const std::string j = camEventJson(c);
|
|
CHECK(j.find("\"cal\":0, \"missed\":2, \"tmax\":null, \"tmed\":null, \"hotx\":null, "
|
|
"\"hoty\":null }") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("numbers in the JSON keep their '.' under a comma locale") {
|
|
// The thermal camera's SDK switches the C locale to one with a decimal comma.
|
|
const std::string before = std::setlocale(LC_NUMERIC, nullptr);
|
|
const bool comma = std::setlocale(LC_NUMERIC, "de_DE.UTF-8") != nullptr;
|
|
CamEvent c;
|
|
c.tower = "T"; c.camera = "TIR";
|
|
ThermalEventFields t;
|
|
t.stats_ok = true; t.hotx = 0.43359; t.hoty = 1.0;
|
|
c.thermal = t;
|
|
const std::string j = camEventJson(c);
|
|
std::setlocale(LC_NUMERIC, before.c_str());
|
|
CHECK(j.find("\"hotx\":0.4336, \"hoty\":1.0000") != std::string::npos);
|
|
if (!comma) MESSAGE("no de_DE locale here: checked under the C locale only");
|
|
HealthReport h;
|
|
h.interval_s = -2.25;
|
|
CHECK(healthJson(h).find("\"interval_s\":-2.3") != std::string::npos); // rounds half away
|
|
}
|
|
|
|
TEST_CASE("CamEvent names are escaped") {
|
|
CamEvent c;
|
|
c.tower = "a\"b"; c.camera = "RGB";
|
|
CHECK(camEventJson(c).find("\"fwt\":\"a\\\"b\"") != std::string::npos);
|
|
}
|