#include #include "fgc/TestRunner.h" #include "fgc/mock/MockImuSource.h" #include "fgc/mock/MockMotorController.h" #include #include using namespace fgc; namespace { // A tiny, fast profile so a full run completes in well under a second. TestProfile fastProfile() { TestProfile p; p.name = "test"; p.params["reps"] = 2; p.params["home_begin_timeout_ms"] = 80; p.params["home_timeout_ms"] = 2000; p.params["settle_timeout_ms"] = 2000; p.params["encoder_diag_reps"] = 1; p.params["hold_window_ms"] = 50; p.params["balance_steps"] = 3; p.params["imu_sample_window_ms"] = 80; p.params["imu_drift_window_ms"] = 80; p.text["backlash_step_counts"] = "500"; return p; } Geometry unitGeometry() { Geometry g; g.yaw.counts_per_deg = 1000; g.yaw.zero_count = 0; g.yaw.min_deg = -90; g.yaw.max_deg = 90; g.pitch.counts_per_deg = 1000; g.pitch.zero_count = 0; g.pitch.min_deg = 0; g.pitch.max_deg = 60; return g; } std::optional runToCompletion(TestRunner& tr, int max_ms = 8000) { REQUIRE(tr.start()); const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(max_ms); while (std::chrono::steady_clock::now() < deadline) { if (auto rep = tr.takeReport()) return rep; std::this_thread::sleep_for(std::chrono::milliseconds(20)); } return std::nullopt; } } // namespace TEST_CASE("resolveTestSelection maps subsystem/test tokens") { uint32_t m = 0; std::string err; CHECK(resolveTestSelection("", "", m, err)); CHECK(m == T_ALL); CHECK(resolveTestSelection("gimbal", "", m, err)); CHECK(m == T_GIMBAL_ALL); CHECK(resolveTestSelection("gimbal", "homing", m, err)); CHECK(m == T_GIMBAL_HOMING); CHECK(resolveTestSelection("host", "disk", m, err)); CHECK(m == T_HOST_DISK); CHECK_FALSE(resolveTestSelection("bogus", "", m, err)); CHECK_FALSE(resolveTestSelection("gimbal", "bogus", m, err)); } TEST_CASE("TestRunner host-only run completes and produces sections") { MockMotorController motor; TestRunner tr(motor, nullptr, unitGeometry(), T_HOST_ALL, fastProfile(), "."); auto rep = runToCompletion(tr); REQUIRE(rep.has_value()); CHECK_FALSE(tr.running()); CHECK(rep->find("host_memory", "ram_avail_mb") != nullptr); CHECK(rep->find("host_disk", "free_gb") != nullptr); } TEST_CASE("TestRunner full run with mocks completes with all selected sections") { MockMotorController motor; MockImuSource imu; imu.start(); TestRunner tr(motor, &imu, unitGeometry(), T_ALL, fastProfile(), "."); auto rep = runToCompletion(tr); REQUIRE(rep.has_value()); auto has = [&](const char* id) { for (const auto& s : rep->sections) if (s.id == id) return true; return false; }; CHECK(has("homing")); CHECK(has("encoder")); CHECK(has("friction")); CHECK(has("backlash")); CHECK(has("balance")); CHECK(has("imu_config")); CHECK(has("imu_health")); CHECK(has("imu_drift")); CHECK(has("host_thermal")); } TEST_CASE("TestRunner is cancellable") { MockMotorController motor; MockImuSource imu; imu.start(); TestProfile p = fastProfile(); p.params["imu_drift_window_ms"] = 5000; // long enough to cancel mid-run TestRunner tr(motor, &imu, unitGeometry(), T_ALL, p, "."); REQUIRE(tr.start()); std::this_thread::sleep_for(std::chrono::milliseconds(50)); tr.cancel(); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(6); while (tr.running() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(20)); CHECK_FALSE(tr.running()); }