213 lines
8.7 KiB
C++
213 lines
8.7 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include "fgc/Paths.h"
|
|
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
#include <unistd.h>
|
|
|
|
using namespace fgc;
|
|
|
|
TEST_CASE("expandUser expands ~ and environment variables") {
|
|
setenv("HOME", "/home/tester", 1);
|
|
setenv("FOO", "bar", 1);
|
|
|
|
CHECK(paths::expandUser("~/x") == "/home/tester/x");
|
|
CHECK(paths::expandUser("~") == "/home/tester");
|
|
CHECK(paths::expandUser("$FOO/y") == "bar/y");
|
|
CHECK(paths::expandUser("${FOO}z") == "barz");
|
|
CHECK(paths::expandUser("/abs/path") == "/abs/path");
|
|
CHECK(paths::expandUser("") == "");
|
|
}
|
|
|
|
TEST_CASE("configSearchPaths honours the CLI argument first") {
|
|
auto p = paths::configSearchPaths("/explicit/config.ini");
|
|
REQUIRE(!p.empty());
|
|
CHECK(p.front() == "/explicit/config.ini");
|
|
}
|
|
|
|
TEST_CASE("the tower folder is searched before the old places") {
|
|
unsetenv("FGC_CONFIG");
|
|
setenv("XDG_CONFIG_HOME", "/home/tester/.config", 1);
|
|
auto p = paths::configSearchPaths("");
|
|
unsetenv("XDG_CONFIG_HOME");
|
|
|
|
// A copy left in the project folder - where a deploy can reach it - must
|
|
// never win over the tower's own.
|
|
REQUIRE(p.size() >= 2);
|
|
CHECK(p[0] == "/home/tester/.config/fire_gimbal_control/config.ini");
|
|
CHECK(p[1] == "config.ini");
|
|
}
|
|
|
|
TEST_CASE("towerConfigDir falls back to ~/.config") {
|
|
unsetenv("XDG_CONFIG_HOME");
|
|
setenv("HOME", "/home/tester", 1);
|
|
CHECK(paths::towerConfigDir() == "/home/tester/.config/fire_gimbal_control");
|
|
}
|
|
|
|
TEST_CASE("an explicit config is never reported as being in the old place") {
|
|
CHECK_FALSE(paths::configInOldPlace("/somewhere/config.ini", "/somewhere/config.ini"));
|
|
}
|
|
|
|
TEST_CASE("hardwareSearchPaths looks in the documented order") {
|
|
unsetenv("FGC_HARDWARE");
|
|
auto p = paths::hardwareSearchPaths("", "/opt/fgc/config.ini");
|
|
|
|
auto at = [&](const std::string& s) {
|
|
for (size_t i = 0; i < p.size(); ++i)
|
|
if (p[i] == s) return static_cast<int>(i);
|
|
return -1;
|
|
};
|
|
// /etc is the normal answer on a tower - root-owned, per-device, and OUTSIDE
|
|
// the deploy tree - so it is searched before a checkout-local file.
|
|
REQUIRE(at("/etc/fire_gimbal_control/hardware.ini") >= 0);
|
|
REQUIRE(at("/opt/fgc/hardware.ini") >= 0);
|
|
CHECK(at("/etc/fire_gimbal_control/hardware.ini") < at("/opt/fgc/hardware.ini"));
|
|
|
|
SUBCASE("an explicit argument outranks everything") {
|
|
auto q = paths::hardwareSearchPaths("/tmp/mine.ini", "/opt/fgc/config.ini");
|
|
CHECK(q.front() == "/tmp/mine.ini");
|
|
}
|
|
SUBCASE("the environment outranks /etc but not the argument") {
|
|
setenv("FGC_HARDWARE", "/tmp/env.ini", 1);
|
|
auto q = paths::hardwareSearchPaths("/tmp/cli.ini", "/opt/fgc/config.ini");
|
|
REQUIRE(q.size() >= 2);
|
|
CHECK(q[0] == "/tmp/cli.ini");
|
|
CHECK(q[1] == "/tmp/env.ini");
|
|
unsetenv("FGC_HARDWARE");
|
|
}
|
|
SUBCASE("with no config path there is simply no sibling candidate") {
|
|
auto q = paths::hardwareSearchPaths("", "");
|
|
for (const auto& s : q) CHECK(s.find("/opt/fgc/") == std::string::npos);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("resolveHardwarePath returns nothing when there is no hardware.ini") {
|
|
// Not an error: every existing tower is in this state until it is migrated,
|
|
// and the program must go on reading everything from config.ini.
|
|
unsetenv("FGC_HARDWARE");
|
|
CHECK_FALSE(paths::resolveHardwarePath("/nonexistent/nope.ini", "").has_value());
|
|
}
|
|
|
|
// --- report pruning: the log dir must not grow without bound ----------------
|
|
|
|
namespace {
|
|
// Point defaultLogDir() at a scratch dir, restoring the environment afterwards
|
|
// even if a REQUIRE aborts the body.
|
|
struct ScopedDataHome {
|
|
std::string old_;
|
|
bool had_ = false;
|
|
std::filesystem::path dir;
|
|
explicit ScopedDataHome(const char* tag) {
|
|
if (const char* p = ::getenv("XDG_DATA_HOME")) { old_ = p; had_ = true; }
|
|
dir = std::filesystem::temp_directory_path() /
|
|
("fgc-prune-" + std::string(tag) + "-" + std::to_string(::getpid()));
|
|
std::filesystem::remove_all(dir);
|
|
std::filesystem::create_directories(dir);
|
|
::setenv("XDG_DATA_HOME", dir.c_str(), 1);
|
|
}
|
|
~ScopedDataHome() {
|
|
if (had_) ::setenv("XDG_DATA_HOME", old_.c_str(), 1);
|
|
else ::unsetenv("XDG_DATA_HOME");
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(dir, ec);
|
|
}
|
|
int countMatching(const std::string& glob_prefix, const std::string& ext) const {
|
|
int n = 0;
|
|
std::error_code ec;
|
|
for (const auto& e : std::filesystem::directory_iterator(fgc::paths::defaultLogDir(), ec)) {
|
|
const std::string name = e.path().filename().string();
|
|
if (name.rfind(glob_prefix, 0) == 0 && e.path().extension() == ext) ++n;
|
|
}
|
|
return n;
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("writing a report prunes older ones of the same kind") {
|
|
ScopedDataHome home("keep");
|
|
for (int i = 0; i < 12; ++i)
|
|
fgc::paths::writeLogFile("test_2026092" + std::to_string(i / 10) +
|
|
"-00000" + std::to_string(i % 10) + ".log",
|
|
"body", /*keep=*/5);
|
|
CHECK(home.countMatching("test_", ".log") == 5);
|
|
}
|
|
|
|
TEST_CASE("only files this program generated are eligible for pruning") {
|
|
// [Test] baseline_file is CONFIGURABLE and may live in this directory. It is
|
|
// the reference every self-test compares against, so pruning it would
|
|
// silently discard the comparison.
|
|
//
|
|
// The default `test_baseline.txt` survives a prefix-only match by accident -
|
|
// 'b' sorts above the digits of a year, so it never lands in the delete
|
|
// range. A baseline named `test_mybaseline.log` does not have that luck, and
|
|
// it is the case a prefix-only match would destroy. Hence: the name must
|
|
// look exactly like timestampedLogName()'s output.
|
|
ScopedDataHome home("baseline");
|
|
std::filesystem::create_directories(fgc::paths::defaultLogDir());
|
|
const auto dir = std::filesystem::path(fgc::paths::defaultLogDir());
|
|
const auto dflt = dir / "test_baseline.txt";
|
|
const auto custom = dir / "test_mybaseline.log"; // sorts BELOW the reports
|
|
{ std::ofstream(dflt) << "pinned"; }
|
|
{ std::ofstream(custom) << "pinned"; }
|
|
|
|
for (int i = 0; i < 10; ++i)
|
|
fgc::paths::writeLogFile("test_20260922-00000" + std::to_string(i) + ".log", "b",
|
|
/*keep=*/2);
|
|
|
|
CHECK(std::filesystem::exists(dflt));
|
|
CHECK(std::filesystem::exists(custom));
|
|
// Exactly the generated reports were pruned, and neither baseline consumed
|
|
// one of the retained slots.
|
|
CHECK(home.countMatching("test_2026", ".log") == 2);
|
|
}
|
|
|
|
TEST_CASE("a name that is not timestampedLogName's shape is left alone") {
|
|
ScopedDataHome home("shape");
|
|
std::filesystem::create_directories(fgc::paths::defaultLogDir());
|
|
const auto dir = std::filesystem::path(fgc::paths::defaultLogDir());
|
|
for (const char* odd : {"test_notes.log", "test_2026-09-22.log", "test_20260922-0000.log"})
|
|
{ std::ofstream(dir / odd) << "x"; }
|
|
|
|
for (int i = 0; i < 6; ++i)
|
|
fgc::paths::writeLogFile("test_20260922-00000" + std::to_string(i) + ".log", "b", 1);
|
|
|
|
for (const char* odd : {"test_notes.log", "test_2026-09-22.log", "test_20260922-0000.log"})
|
|
CHECK(std::filesystem::exists(dir / odd));
|
|
// Narrow enough to match ONLY the generated reports: "test_20260922-0000.log"
|
|
// is one of the planted oddities and shares a shorter prefix with them.
|
|
CHECK(home.countMatching("test_20260922-00000", ".log") == 1);
|
|
}
|
|
|
|
TEST_CASE("pruning one kind leaves the others alone") {
|
|
ScopedDataHome home("kinds");
|
|
for (int i = 0; i < 4; ++i)
|
|
fgc::paths::writeLogFile("diag_20260922-00000" + std::to_string(i) + ".log", "d", 10);
|
|
for (int i = 0; i < 6; ++i)
|
|
fgc::paths::writeLogFile("test_20260922-00000" + std::to_string(i) + ".log", "t", 2);
|
|
|
|
CHECK(home.countMatching("test_", ".log") == 2); // pruned
|
|
CHECK(home.countMatching("diag_", ".log") == 4); // untouched
|
|
}
|
|
|
|
TEST_CASE("the newest report survives, and it is the one just written") {
|
|
ScopedDataHome home("newest");
|
|
std::string last;
|
|
for (int i = 0; i < 8; ++i)
|
|
last = fgc::paths::writeLogFile("test_20260922-00000" + std::to_string(i) + ".log",
|
|
"body-" + std::to_string(i), /*keep=*/3);
|
|
REQUIRE_FALSE(last.empty());
|
|
CHECK(std::filesystem::exists(last));
|
|
}
|
|
|
|
TEST_CASE("keep = 0 opts out, preserving the old unbounded behaviour") {
|
|
ScopedDataHome home("optout");
|
|
for (int i = 0; i < 7; ++i)
|
|
fgc::paths::writeLogFile("test_20260922-00000" + std::to_string(i) + ".log", "b",
|
|
/*keep=*/0);
|
|
CHECK(home.countMatching("test_", ".log") == 7);
|
|
}
|