fwt_software/tests/test_hardwarepresence.cpp

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);
}