121 lines
3.9 KiB
C++
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);
|
|
}
|