fwt_software/tests/test_singleinstance.cpp

121 lines
3.9 KiB
C++

#include <doctest/doctest.h>
#include "fgc/SingleInstance.h"
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include <unistd.h>
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<bool>(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<bool>(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);
}