#include #include "fgc/SingleInstance.h" #include #include #include #include #include using namespace fgc; namespace { // A unique lockfile path per test case, cleaned up by the caller. std::string tempLockPath(const char* tag) { return (std::filesystem::temp_directory_path() / ("fgc_test_" + std::string(tag) + "_" + std::to_string(::getpid()) + ".lock")) .string(); } } // namespace TEST_CASE("acquire succeeds on a fresh path and records our PID") { const std::string path = tempLockPath("fresh"); std::filesystem::remove(path); auto lock = SingleInstance::acquire(path); REQUIRE(lock.held()); CHECK(static_cast(lock)); CHECK(lock.error().empty()); CHECK(lock.path() == path); std::ifstream in(path); int pid = 0; in >> pid; CHECK(pid == ::getpid()); lock.release(); std::filesystem::remove(path); } TEST_CASE("a second acquire on the same path is refused") { // flock(2) is per open-file-description, not per process, so a second // open+flock from this same test binary contends exactly as another // process would - which is what makes this testable without forking. const std::string path = tempLockPath("contend"); std::filesystem::remove(path); auto first = SingleInstance::acquire(path); REQUIRE(first.held()); auto second = SingleInstance::acquire(path); CHECK_FALSE(second.held()); CHECK_FALSE(static_cast(second)); CHECK(second.error().find("already running") != std::string::npos); CHECK(second.holderPid() == ::getpid()); first.release(); std::filesystem::remove(path); } TEST_CASE("releasing lets a later acquire through") { const std::string path = tempLockPath("release"); std::filesystem::remove(path); { auto lock = SingleInstance::acquire(path); REQUIRE(lock.held()); } // destructor releases auto again = SingleInstance::acquire(path); CHECK(again.held()); again.release(); CHECK_FALSE(again.held()); again.release(); // idempotent std::filesystem::remove(path); } TEST_CASE("moving transfers the lock without releasing it") { const std::string path = tempLockPath("move"); std::filesystem::remove(path); auto lock = SingleInstance::acquire(path); REQUIRE(lock.held()); auto moved = std::move(lock); CHECK(moved.held()); CHECK_FALSE(lock.held()); // NOLINT(bugprone-use-after-move) - checking the moved-from state // Still locked against a fresh attempt. CHECK_FALSE(SingleInstance::acquire(path).held()); moved.release(); std::filesystem::remove(path); } TEST_CASE("acquire reports an unopenable lockfile instead of claiming the lock") { auto lock = SingleInstance::acquire("/nonexistent-dir-fgc-test/x.lock"); CHECK_FALSE(lock.held()); CHECK(lock.error().find("cannot open") != std::string::npos); } TEST_CASE("defaultLockPath is per-uid and independent of the environment") { const std::string uid = std::to_string(::getuid()); const std::string run = "/run/user/" + uid; const std::string expected = std::filesystem::is_directory(run) ? run + "/fire_gimbal_control.lock" : "/tmp/fire_gimbal_control-" + uid + ".lock"; CHECK(SingleInstance::defaultLockPath() == expected); // $XDG_RUNTIME_DIR is set in an interactive ssh shell but not in a // non-interactive `ssh host cmd` one. If the path tracked it, those two // launch routes would take different locks and neither would exclude the // other - so the path must not move when the variable does. setenv("XDG_RUNTIME_DIR", "/run/user/4242", 1); CHECK(SingleInstance::defaultLockPath() == expected); unsetenv("XDG_RUNTIME_DIR"); CHECK(SingleInstance::defaultLockPath() == expected); }