Skip to content

Commit 1d4cbd2

Browse files
committed
test: Add unit test for LockDirectory
Add a unit test for LockDirectory, introduced in #11281.
1 parent fc888bf commit 1d4cbd2

File tree

3 files changed

+154
-9
lines changed

3 files changed

+154
-9
lines changed

src/test/util_tests.cpp

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
#include <stdint.h>
1515
#include <vector>
16+
#ifndef WIN32
17+
#include <sys/types.h>
18+
#include <sys/wait.h>
19+
#endif
1620

1721
#include <boost/test/unit_test.hpp>
1822

@@ -603,4 +607,130 @@ BOOST_AUTO_TEST_CASE(test_ParseFixedPoint)
603607
BOOST_CHECK(!ParseFixedPoint("1.", 8, &amount));
604608
}
605609

610+
static void TestOtherThread(fs::path dirname, std::string lockname, bool *result)
611+
{
612+
*result = LockDirectory(dirname, lockname);
613+
}
614+
615+
#ifndef WIN32 // Cannot do this test on WIN32 due to lack of fork()
616+
static constexpr char LockCommand = 'L';
617+
static constexpr char UnlockCommand = 'U';
618+
static constexpr char ExitCommand = 'X';
619+
620+
static void TestOtherProcess(fs::path dirname, std::string lockname, int fd)
621+
{
622+
char ch;
623+
int rv;
624+
while (true) {
625+
rv = read(fd, &ch, 1); // Wait for command
626+
assert(rv == 1);
627+
switch(ch) {
628+
case LockCommand:
629+
ch = LockDirectory(dirname, lockname);
630+
rv = write(fd, &ch, 1);
631+
assert(rv == 1);
632+
break;
633+
case UnlockCommand:
634+
ReleaseDirectoryLocks();
635+
ch = true; // Always succeeds
636+
rv = write(fd, &ch, 1);
637+
break;
638+
case ExitCommand:
639+
close(fd);
640+
exit(0);
641+
default:
642+
assert(0);
643+
}
644+
}
645+
}
646+
#endif
647+
648+
BOOST_AUTO_TEST_CASE(test_LockDirectory)
649+
{
650+
fs::path dirname = fs::temp_directory_path() / fs::unique_path();
651+
const std::string lockname = ".lock";
652+
#ifndef WIN32
653+
// Revert SIGCHLD to default, otherwise boost.test will catch and fail on
654+
// it: there is BOOST_TEST_IGNORE_SIGCHLD but that only works when defined
655+
// at build-time of the boost library
656+
void (*old_handler)(int) = signal(SIGCHLD, SIG_DFL);
657+
658+
// Fork another process for testing before creating the lock, so that we
659+
// won't fork while holding the lock (which might be undefined, and is not
660+
// relevant as test case as that is avoided with -daemonize).
661+
int fd[2];
662+
BOOST_CHECK_EQUAL(socketpair(AF_UNIX, SOCK_STREAM, 0, fd), 0);
663+
pid_t pid = fork();
664+
if (!pid) {
665+
BOOST_CHECK_EQUAL(close(fd[1]), 0); // Child: close parent end
666+
TestOtherProcess(dirname, lockname, fd[0]);
667+
}
668+
BOOST_CHECK_EQUAL(close(fd[0]), 0); // Parent: close child end
669+
#endif
670+
// Lock on non-existent directory should fail
671+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), false);
672+
673+
fs::create_directories(dirname);
674+
675+
// Probing lock on new directory should succeed
676+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true);
677+
678+
// Persistent lock on new directory should succeed
679+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), true);
680+
681+
// Another lock on the directory from the same thread should succeed
682+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), true);
683+
684+
// Another lock on the directory from a different thread within the same process should succeed
685+
bool threadresult;
686+
std::thread thr(TestOtherThread, dirname, lockname, &threadresult);
687+
thr.join();
688+
BOOST_CHECK_EQUAL(threadresult, true);
689+
#ifndef WIN32
690+
// Try to aquire lock in child process while we're holding it, this should fail.
691+
char ch;
692+
BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1);
693+
BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1);
694+
BOOST_CHECK_EQUAL((bool)ch, false);
695+
696+
// Give up our lock
697+
ReleaseDirectoryLocks();
698+
// Probing lock from our side now should succeed, but not hold on to the lock.
699+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true);
700+
701+
// Try to acquire the lock in the child process, this should be succesful.
702+
BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1);
703+
BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1);
704+
BOOST_CHECK_EQUAL((bool)ch, true);
705+
706+
// When we try to probe the lock now, it should fail.
707+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), false);
708+
709+
// Unlock the lock in the child process
710+
BOOST_CHECK_EQUAL(write(fd[1], &UnlockCommand, 1), 1);
711+
BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1);
712+
BOOST_CHECK_EQUAL((bool)ch, true);
713+
714+
// When we try to probe the lock now, it should succeed.
715+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true);
716+
717+
// Re-lock the lock in the child process, then wait for it to exit, check
718+
// successful return. After that, we check that exiting the process
719+
// has released the lock as we would expect by probing it.
720+
int processstatus;
721+
BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1);
722+
BOOST_CHECK_EQUAL(write(fd[1], &ExitCommand, 1), 1);
723+
BOOST_CHECK_EQUAL(waitpid(pid, &processstatus, 0), pid);
724+
BOOST_CHECK_EQUAL(processstatus, 0);
725+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true);
726+
727+
// Restore SIGCHLD
728+
signal(SIGCHLD, old_handler);
729+
BOOST_CHECK_EQUAL(close(fd[1]), 0); // Close our side of the socketpair
730+
#endif
731+
// Clean up
732+
ReleaseDirectoryLocks();
733+
fs::remove_all(dirname);
734+
}
735+
606736
BOOST_AUTO_TEST_SUITE_END()

src/util.cpp

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -373,19 +373,22 @@ int LogPrintStr(const std::string &str)
373373
return ret;
374374
}
375375

376+
/** A map that contains all the currently held directory locks. After
377+
* successful locking, these will be held here until the global destructor
378+
* cleans them up and thus automatically unlocks them, or ReleaseDirectoryLocks
379+
* is called.
380+
*/
381+
static std::map<std::string, std::unique_ptr<boost::interprocess::file_lock>> dir_locks;
382+
/** Mutex to protect dir_locks. */
383+
static std::mutex cs_dir_locks;
384+
376385
bool LockDirectory(const fs::path& directory, const std::string lockfile_name, bool probe_only)
377386
{
378-
// A map that contains all the currently held directory locks. After
379-
// successful locking, these will be held here until the global
380-
// destructor cleans them up and thus automatically unlocks them.
381-
static std::map<std::string, std::unique_ptr<boost::interprocess::file_lock>> locks;
382-
// Protect the map with a mutex
383-
static std::mutex cs;
384-
std::lock_guard<std::mutex> ulock(cs);
387+
std::lock_guard<std::mutex> ulock(cs_dir_locks);
385388
fs::path pathLockFile = directory / lockfile_name;
386389

387390
// If a lock for this directory already exists in the map, don't try to re-lock it
388-
if (locks.count(pathLockFile.string())) {
391+
if (dir_locks.count(pathLockFile.string())) {
389392
return true;
390393
}
391394

@@ -400,14 +403,20 @@ bool LockDirectory(const fs::path& directory, const std::string lockfile_name, b
400403
}
401404
if (!probe_only) {
402405
// Lock successful and we're not just probing, put it into the map
403-
locks.emplace(pathLockFile.string(), std::move(lock));
406+
dir_locks.emplace(pathLockFile.string(), std::move(lock));
404407
}
405408
} catch (const boost::interprocess::interprocess_exception& e) {
406409
return error("Error while attempting to lock directory %s: %s", directory.string(), e.what());
407410
}
408411
return true;
409412
}
410413

414+
void ReleaseDirectoryLocks()
415+
{
416+
std::lock_guard<std::mutex> ulock(cs_dir_locks);
417+
dir_locks.clear();
418+
}
419+
411420
/** Interpret string as boolean, for argument parsing */
412421
static bool InterpretBool(const std::string& strValue)
413422
{

src/util.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ int RaiseFileDescriptorLimit(int nMinFD);
174174
void AllocateFileRange(FILE *file, unsigned int offset, unsigned int length);
175175
bool RenameOver(fs::path src, fs::path dest);
176176
bool LockDirectory(const fs::path& directory, const std::string lockfile_name, bool probe_only=false);
177+
178+
/** Release all directory locks. This is used for unit testing only, at runtime
179+
* the global destructor will take care of the locks.
180+
*/
181+
void ReleaseDirectoryLocks();
182+
177183
bool TryCreateDirectories(const fs::path& p);
178184
fs::path GetDefaultDataDir();
179185
const fs::path &GetDataDir(bool fNetSpecific = true);

0 commit comments

Comments
 (0)