#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]") { ScopedTemporaryFile lock_file(".lock"); const std::string path = lock_file.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]") { ScopedTemporaryFile lock_file(".lock"); const std::string path = lock_file.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 retries a lock file it could not open once the cool-down passes", "[InstanceLock]") { ScopedTemporaryDir dir; const std::string path = (dir.path() / "later" / "shared.lock").string(); const auto saved_cooldown = InstanceLock::cooldown; InstanceLock::cooldown = 300ms; bool before_dir, during_cooldown, after_cooldown; const auto started = std::chrono::steady_clock::now(); { InstanceLock lock(path, 100ms); before_dir = lock.locked(); } boost::filesystem::create_directories(dir.path() / "later"); { InstanceLock lock(path, 100ms); during_cooldown = lock.locked(); } const bool second_guard_inside_cooldown = std::chrono::steady_clock::now() - started < InstanceLock::cooldown; std::this_thread::sleep_for(400ms); { InstanceLock lock(path, 100ms); after_cooldown = lock.locked(); } InstanceLock::cooldown = saved_cooldown; REQUIRE_FALSE(before_dir); // A loaded runner may take longer than the cool-down to get here; then the // second guard legitimately retried, so only assert when the timing held. if (second_guard_inside_cooldown) REQUIRE_FALSE(during_cooldown); REQUIRE(after_cooldown); } TEST_CASE("InstanceLock reopens a lock file that was replaced on disk", "[InstanceLock]") { ScopedTemporaryFile lock_file(".lock"); const std::string path = lock_file.string(); const auto saved_interval = InstanceLock::identity_check_interval; InstanceLock::identity_check_interval = 0ms; { InstanceLock lock(path); REQUIRE(lock.locked()); } boost::filesystem::remove(path); InstanceLock lock(path); InstanceLock::identity_check_interval = saved_interval; REQUIRE(lock.locked()); // Only a reopen recreates the file; a guard still holding the unlinked one // would leave the path missing. (The inode number itself may be reused once // the old handle is closed, so it is no proof either way.) REQUIRE(boost::filesystem::exists(path)); } TEST_CASE("InstanceLock serialises the threads of one process", "[InstanceLock]") { ScopedTemporaryFile lock_file(".lock"); const std::string path = lock_file.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]") { ScopedTemporaryFile lock_file(".lock"); const std::string path = lock_file.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(); } // The timed-out wait starts a cool-down: the next guard does not wait again. const auto started = std::chrono::steady_clock::now(); bool locked_during_cooldown; { InstanceLock lock(path, 5000ms); locked_during_cooldown = lock.locked(); } const auto cooldown_wait = std::chrono::steady_clock::now() - started; 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); REQUIRE_FALSE(locked_during_cooldown); REQUIRE(cooldown_wait < 1000ms); // A guard inside the cool-down still takes the lock when it is free. InstanceLock lock(path); REQUIRE(lock.locked()); } #endif