fwt_software/tests/test_operatingmode.cpp

123 lines
4.5 KiB
C++

#include <doctest/doctest.h>
#include "fgc/OperatingMode.h"
#include <string>
#include <vector>
using namespace fgc;
namespace {
std::vector<std::string> tok(const std::string& line) {
std::vector<std::string> out;
std::string w;
for (char c : line) {
if (c == ' ') { if (!w.empty()) out.push_back(w); w.clear(); }
else w += c;
}
if (!w.empty()) out.push_back(w);
return out;
}
} // namespace
// --- pan positions --------------------------------------------------------------
TEST_CASE("the user's example: 90 +/- 15 at 6 degrees is six pictures, 75 to 105") {
const auto h = panHeadings({90.0, 30.0, 0.0}, 6.0);
REQUIRE(h.size() == 6);
const double want[] = {75, 81, 87, 93, 99, 105};
for (size_t i = 0; i < h.size(); ++i) CHECK(h[i] == doctest::Approx(want[i]));
}
TEST_CASE("a span that does not divide evenly gets one more picture, never a gap") {
const auto h = panHeadings({90.0, 32.0, 0.0}, 6.0);
REQUIRE(h.size() == 7);
CHECK(h.front() == doctest::Approx(74.0));
CHECK(h.back() == doctest::Approx(106.0));
for (size_t i = 1; i < h.size(); ++i) CHECK(h[i] - h[i - 1] <= 6.0 + 1e-9);
}
TEST_CASE("a span narrower than one picture is the one heading") {
CHECK(panHeadings({90.0, 5.9, 0.0}, 6.0) == std::vector<double>{90.0});
CHECK(panHeadings({90.0, 0.0, 0.0}, 6.0) == std::vector<double>{90.0});
}
TEST_CASE("exactly one picture wide is its two edges") {
const auto h = panHeadings({90.0, 6.0, 0.0}, 6.0);
REQUIRE(h.size() == 2);
CHECK(h[0] == doctest::Approx(87.0));
CHECK(h[1] == doctest::Approx(93.0));
}
TEST_CASE("a pan across north wraps its bearings") {
const auto h = panHeadings({5.0, 30.0, 0.0}, 6.0);
REQUIRE(h.size() == 6);
CHECK(h.front() == doctest::Approx(350.0));
CHECK(h[2] == doctest::Approx(2.0));
CHECK(h.back() == doctest::Approx(20.0));
}
// --- the command ----------------------------------------------------------------
TEST_CASE("mode pan: pitch is optional and defaults to level") {
auto r = parseModeCommand(tok("mode pan 90 30"));
REQUIRE(r.valid);
CHECK(r.mode == Mode::Pan);
CHECK(r.pan.heading == 90.0);
CHECK(r.pan.span == 30.0);
CHECK(r.pan.pitch == 0.0);
r = parseModeCommand(tok("mode pan 450 30 10"));
REQUIRE(r.valid);
CHECK(r.pan.heading == doctest::Approx(90.0)); // bearings wrap
CHECK(r.pan.pitch == 10.0);
}
TEST_CASE("mode auto, mode manual, and a bare mode that only asks") {
CHECK(parseModeCommand(tok("mode auto")).mode == Mode::Auto);
CHECK(parseModeCommand(tok("mode MANUAL")).mode == Mode::Manual);
const auto q = parseModeCommand(tok("mode"));
CHECK(q.valid);
CHECK(q.query);
}
TEST_CASE("a bad mode command is refused with the reason, never half-applied") {
for (const char* bad : {"mode pan", "mode pan 90", "mode pan x 30", "mode pan 90 30x",
"mode pan 90 -1", "mode pan 90 361", "mode pan 90 30 up",
"mode pan 90 30 10 4", "mode sweep", "mode auto now"}) {
CAPTURE(bad);
const auto r = parseModeCommand(tok(bad));
CHECK_FALSE(r.valid);
CHECK_FALSE(r.error.empty());
}
}
// --- what each mode accepts -------------------------------------------------------
TEST_CASE("auto and pan accept only what cannot move the gimbal or change a setting") {
for (Mode m : {Mode::Auto, Mode::Pan}) {
CAPTURE(modeName(m));
for (const char* ok : {"help", "help imu", "gimbal status", "gimbal dump", "imu status",
"imu config", "refresh", "debug", "trace serial on", "mode",
"mode manual", "mode pan 90 30", "cancel", "exit", "GIMBAL DUMP"}) {
CAPTURE(ok);
CHECK(allowedIn(m, ok));
}
for (const char* no : {"gimbal nudge yaw 5", "gimbal move 10,0", "gimbal raw \"HOME\"",
"gimbal home", "gimbal stop", "gimbal heading offset 2",
"gimbal speed y 1000", "set fps 1", "set camera profile night",
"imu zero", "imu profile general", "test", "test hardware",
"camera reset", "snapshot", "calib save", "gimbal"}) {
CAPTURE(no);
CHECK_FALSE(allowedIn(m, no));
}
}
}
TEST_CASE("manual accepts everything") {
for (const char* any : {"gimbal nudge yaw 5", "gimbal raw \"HOME\"", "set fps 1", "test",
"camera reset", "exit", "anything at all"})
CHECK(allowedIn(Mode::Manual, any));
}