205 lines
7.0 KiB
C++
205 lines
7.0 KiB
C++
#include "doctest/doctest.h"
|
|
#include "fgc/HardwarePresence.h"
|
|
|
|
#include <algorithm>
|
|
|
|
using namespace fgc;
|
|
|
|
namespace {
|
|
|
|
// A 2-axis Delta tower with both sensors and one camera, as hardware.ini
|
|
// declares it.
|
|
HardwareConfig declaredTower() {
|
|
HardwareConfig h;
|
|
h.declared = true;
|
|
h.source_path = "/etc/fire_gimbal_control/hardware.ini";
|
|
h.axes = {"yaw", "pitch"};
|
|
h.imu = "mti-630";
|
|
h.env = "sht41";
|
|
h.cameras = {"rgb"};
|
|
return h;
|
|
}
|
|
|
|
// The same tower, working.
|
|
FoundHardware foundTower() {
|
|
FoundHardware f;
|
|
f.axes = {"yaw", "pitch"};
|
|
f.imu = true;
|
|
f.env = true;
|
|
f.cameras = {"rgb"};
|
|
f.cameras_opened = 1;
|
|
return f;
|
|
}
|
|
|
|
bool mentions(const PresenceReport& r, const std::string& subsystem) {
|
|
return std::any_of(r.findings.begin(), r.findings.end(),
|
|
[&](const PresenceFinding& f) { return f.subsystem == subsystem; });
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("an undeclared tower is never reconciled") {
|
|
// Every tower is in this state until it gains a hardware.ini, and gaining
|
|
// one must not be what changes its behaviour.
|
|
HardwareConfig none; // declared == false
|
|
FoundHardware bare; // nothing found at all
|
|
bare.axes = {"yaw"};
|
|
|
|
auto r = reconcilePresence(none, bare);
|
|
CHECK_FALSE(r.reconciled);
|
|
CHECK(r.findings.empty());
|
|
CHECK(presenceVerdict(r, /*strict=*/true, /*degraded=*/false).action == PresenceAction::Proceed);
|
|
}
|
|
|
|
TEST_CASE("a matching rig produces no findings") {
|
|
auto r = reconcilePresence(declaredTower(), foundTower());
|
|
CHECK(r.reconciled);
|
|
CHECK(r.findings.empty());
|
|
CHECK_FALSE(r.anyMissing());
|
|
CHECK(presenceVerdict(r, true, false).action == PresenceAction::Proceed);
|
|
}
|
|
|
|
TEST_CASE("the bug this exists to catch: a declared axis that stopped enumerating") {
|
|
// The 2-axis tower whose pitch vanished. Before reconciliation this looked
|
|
// exactly like a healthy yaw-only tower.
|
|
auto found = foundTower();
|
|
found.axes = {"yaw"};
|
|
|
|
auto r = reconcilePresence(declaredTower(), found);
|
|
REQUIRE(r.anyMissing());
|
|
CHECK(mentions(r, "axis pitch"));
|
|
CHECK(r.missing() == std::vector<std::string>{"axis pitch"});
|
|
}
|
|
|
|
TEST_CASE("an axis found but not declared is a stale file, never fatal") {
|
|
auto declared = declaredTower();
|
|
declared.axes = {"yaw"}; // the file still describes the old single-axis rig
|
|
|
|
auto r = reconcilePresence(declared, foundTower());
|
|
CHECK(r.anyUndeclared());
|
|
CHECK_FALSE(r.anyMissing());
|
|
CHECK(mentions(r, "axis pitch"));
|
|
|
|
// Strict, no --degraded: still only a warning. More hardware than declared is
|
|
// a documentation fault, not a broken tower.
|
|
auto v = presenceVerdict(r, /*strict=*/true, /*degraded=*/false);
|
|
CHECK(v.action == PresenceAction::WarnOnly);
|
|
}
|
|
|
|
TEST_CASE("an empty axes list states nothing and is not reconciled") {
|
|
// Matches hasAxis(), which answers true for everything when nothing is said.
|
|
auto declared = declaredTower();
|
|
declared.axes = {};
|
|
auto found = foundTower();
|
|
found.axes = {"yaw"};
|
|
|
|
auto r = reconcilePresence(declared, found);
|
|
CHECK_FALSE(mentions(r, "axis yaw"));
|
|
CHECK_FALSE(mentions(r, "axis pitch"));
|
|
}
|
|
|
|
TEST_CASE("no telemetry at all names the link, not every axis") {
|
|
auto found = foundTower();
|
|
found.axes = {};
|
|
found.axes_known = false;
|
|
|
|
auto r = reconcilePresence(declaredTower(), found);
|
|
REQUIRE(r.anyMissing());
|
|
CHECK(mentions(r, "motor telemetry"));
|
|
// The useful fault is the serial link; listing both axes would bury it.
|
|
CHECK_FALSE(mentions(r, "axis yaw"));
|
|
CHECK_FALSE(mentions(r, "axis pitch"));
|
|
}
|
|
|
|
TEST_CASE("sensors distinguish 'not fitted' from 'not stated'") {
|
|
SUBCASE("declared and missing is a fault") {
|
|
auto found = foundTower();
|
|
found.imu = false;
|
|
auto r = reconcilePresence(declaredTower(), found);
|
|
CHECK(mentions(r, "imu"));
|
|
CHECK(r.anyMissing());
|
|
}
|
|
SUBCASE("declared 'none' but running is a stale file") {
|
|
auto declared = declaredTower();
|
|
declared.imu = "none";
|
|
auto r = reconcilePresence(declared, foundTower());
|
|
CHECK(mentions(r, "imu"));
|
|
CHECK_FALSE(r.anyMissing());
|
|
CHECK(r.anyUndeclared());
|
|
}
|
|
SUBCASE("not stated at all is silent in both directions") {
|
|
auto declared = declaredTower();
|
|
declared.imu = "";
|
|
declared.env = "";
|
|
auto found = foundTower();
|
|
found.env = false;
|
|
auto r = reconcilePresence(declared, found);
|
|
CHECK_FALSE(mentions(r, "imu"));
|
|
CHECK_FALSE(mentions(r, "env"));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("a camera that is configured but never opened is missing") {
|
|
auto found = foundTower();
|
|
found.cameras_opened = 0; // configured, declared, not on the bus
|
|
|
|
auto r = reconcilePresence(declaredTower(), found);
|
|
REQUIRE(r.anyMissing());
|
|
CHECK(mentions(r, "cameras"));
|
|
}
|
|
|
|
TEST_CASE("an unasked camera count is not a shortfall") {
|
|
auto found = foundTower();
|
|
found.cameras_opened = -1; // no camera source in this run
|
|
|
|
auto r = reconcilePresence(declaredTower(), found);
|
|
CHECK_FALSE(mentions(r, "cameras"));
|
|
}
|
|
|
|
TEST_CASE("the rollout default is warn-only, and says how to tighten it") {
|
|
auto found = foundTower();
|
|
found.axes = {"yaw"};
|
|
auto r = reconcilePresence(declaredTower(), found);
|
|
|
|
auto v = presenceVerdict(r, /*strict=*/false, /*degraded=*/false);
|
|
CHECK(v.action == PresenceAction::WarnOnly);
|
|
CHECK(v.reason.find("strict_presence") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("strict refuses, and names both the fault and the escape") {
|
|
auto found = foundTower();
|
|
found.axes = {"yaw"};
|
|
auto r = reconcilePresence(declaredTower(), found);
|
|
|
|
auto v = presenceVerdict(r, /*strict=*/true, /*degraded=*/false);
|
|
CHECK(v.action == PresenceAction::Refuse);
|
|
CHECK(v.reason.find("axis pitch") != std::string::npos);
|
|
CHECK(v.reason.find("--degraded") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("--degraded overrides the refusal, and is loud about it") {
|
|
auto found = foundTower();
|
|
found.axes = {"yaw"};
|
|
auto r = reconcilePresence(declaredTower(), found);
|
|
|
|
auto v = presenceVerdict(r, /*strict=*/true, /*degraded=*/true);
|
|
CHECK(v.action == PresenceAction::WarnOnly);
|
|
// It must not read like a normal startup line.
|
|
CHECK(v.reason.find("DEGRADED") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("formatting puts the faults before the stale-file warnings") {
|
|
auto declared = declaredTower();
|
|
declared.axes = {"yaw", "pitch"};
|
|
auto found = foundTower();
|
|
found.axes = {"yaw"}; // pitch missing -> a fault
|
|
found.imu = true;
|
|
declared.imu = "none"; // imu undeclared -> stale file
|
|
|
|
auto r = reconcilePresence(declared, found);
|
|
auto lines = formatPresence(r);
|
|
REQUIRE(lines.size() == 2);
|
|
CHECK(lines[0].rfind("axis pitch:", 0) == 0);
|
|
CHECK(lines[1].rfind("imu:", 0) == 0);
|
|
}
|