#include #include #include #include #include #include "libslic3r/InstanceLock.hpp" #include "test_utils.hpp" #ifndef _WIN32 #include #include #include #endif using namespace Slic3r; using namespace std::chrono_literals; TEST_CASE("InstanceLock creates its lock file and holds it for the guard's scope", "[InstanceLock]") { ScopedTemporaryDir dir; const std::string path = (dir.path() / "shared.lock").string(); { InstanceLock lock(path); REQUIRE(lock.locked()); REQUIRE(boost::filesystem::exists(path)); } // Released: a fresh guard gets the lock at once instead of waiting out a timeout. const auto started = std::chrono::steady_clock::now(); InstanceLock again(path, 5000ms); REQUIRE(again.locked()); REQUIRE(std::chrono::steady_clock::now() - started < 1000ms); } TEST_CASE("InstanceLock nests within one thread", "[InstanceLock]") { ScopedTemporaryDir dir; const std::string path = (dir.path() / "shared.lock").string(); InstanceLock outer(path); { InstanceLock inner(path, 100ms); REQUIRE(inner.locked()); } // The inner guard leaving does not release the outer one. REQUIRE(outer.locked()); } TEST_CASE("InstanceLock is a no-op for an empty path and survives an unwritable one", "[InstanceLock]") { ScopedTemporaryDir dir; InstanceLock none(""); REQUIRE_FALSE(none.locked()); // The directory does not exist, so the lock file cannot be created; the // guard still constructs and the write it guards can go ahead. InstanceLock unwritable((dir.path() / "missing" / "shared.lock").string(), 100ms); REQUIRE_FALSE(unwritable.locked()); } TEST_CASE("InstanceLock serialises the threads of one process", "[InstanceLock]") { ScopedTemporaryDir dir; const std::string path = (dir.path() / "shared.lock").string(); std::atomic holder_ready{false}; std::atomic holder_released{false}; std::thread holder([&] { InstanceLock lock(path); holder_ready = true; std::this_thread::sleep_for(150ms); holder_released = true; }); while (! holder_ready) std::this_thread::yield(); bool released_before_acquire = false; { InstanceLock lock(path); released_before_acquire = holder_released; } holder.join(); REQUIRE(released_before_acquire); } #ifndef _WIN32 // The cross-process side of the lock is a POSIX fcntl write lock, which a // child process takes here directly; the same primitive backs the guard on // Windows through LockFileEx, but spawning a child there is not worth a test. TEST_CASE("InstanceLock yields to another process and reports it", "[InstanceLock]") { ScopedTemporaryDir dir; const std::string path = (dir.path() / "shared.lock").string(); int child_holds[2], child_may_exit[2]; REQUIRE(::pipe(child_holds) == 0); REQUIRE(::pipe(child_may_exit) == 0); const pid_t child = ::fork(); REQUIRE(child >= 0); if (child == 0) { int fd = ::open(path.c_str(), O_RDWR | O_CREAT, 0644); struct flock lock{}; lock.l_type = F_WRLCK; lock.l_whence = SEEK_SET; char byte = ::fcntl(fd, F_SETLK, &lock) == 0 ? '1' : '0'; if (::write(child_holds[1], &byte, 1) != 1 || ::read(child_may_exit[0], &byte, 1) != 1) ::_exit(1); ::_exit(0); } char byte = '0'; REQUIRE(::read(child_holds[0], &byte, 1) == 1); REQUIRE(byte == '1'); bool locked_while_child_holds; { InstanceLock lock(path, 100ms); locked_while_child_holds = lock.locked(); } REQUIRE(::write(child_may_exit[1], "x", 1) == 1); int status = 0; REQUIRE(::waitpid(child, &status, 0) == child); for (int fd : {child_holds[0], child_holds[1], child_may_exit[0], child_may_exit[1]}) ::close(fd); REQUIRE_FALSE(locked_while_child_holds); InstanceLock lock(path); REQUIRE(lock.locked()); } #endif