mirror of
https://github.com/transmission/transmission.git
synced 2025-12-20 02:18:42 +00:00
refactor: watchdir (#3606)
This commit is contained in:
@@ -99,7 +99,7 @@ TEST_P(SubprocessTest, SpawnAsyncMissingExec)
|
||||
TEST_P(SubprocessTest, SpawnAsyncArgs)
|
||||
{
|
||||
auto const result_path = buildSandboxPath("result.txt");
|
||||
bool const allow_batch_metachars = TR_IF_WIN32(false, true) || !tr_str_has_suffix(self_path_.c_str(), ".cmd");
|
||||
bool const allow_batch_metachars = TR_IF_WIN32(false, true) || !tr_strvEndsWith(tr_strlower(self_path_), ".cmd"sv);
|
||||
|
||||
auto const test_arg1 = std::string{ "arg1 " };
|
||||
auto const test_arg2 = std::string{ " arg2" };
|
||||
|
||||
@@ -256,6 +256,8 @@ protected:
|
||||
0600,
|
||||
nullptr);
|
||||
blockingFileWrite(fd, payload, n);
|
||||
tr_sys_file_flush(fd);
|
||||
tr_sys_file_flush(fd);
|
||||
tr_sys_file_close(fd);
|
||||
sync();
|
||||
|
||||
|
||||
@@ -3,36 +3,38 @@
|
||||
// or any future license endorsed by Mnemosyne LLC.
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define LIBTRANSMISSION_WATCHDIR_MODULE
|
||||
|
||||
#include "transmission.h"
|
||||
|
||||
#include "file.h"
|
||||
#include "net.h"
|
||||
#include "watchdir.h"
|
||||
#include "watchdir-base.h"
|
||||
#include "timer-ev.h"
|
||||
|
||||
#include "test-fixtures.h"
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
/***
|
||||
****
|
||||
***/
|
||||
|
||||
extern struct timeval tr_watchdir_generic_interval;
|
||||
extern size_t tr_watchdir_retry_limit;
|
||||
extern struct timeval tr_watchdir_retry_start_interval;
|
||||
extern struct timeval tr_watchdir_retry_max_interval;
|
||||
static auto constexpr GenericRescanInterval = 100ms;
|
||||
static auto constexpr RetryDuration = 100ms;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
auto constexpr FiftyMsec = timeval{ 0, 50000 };
|
||||
auto constexpr OneHundredMsec = timeval{ 0, 100000 };
|
||||
auto constexpr TwoHundredMsec = timeval{ 0, 200000 };
|
||||
|
||||
} // namespace
|
||||
// should be at least 2x the watchdir-generic size to ensure that
|
||||
// we have time to pump all events at least once in processEvents()
|
||||
static auto constexpr ProcessEventsTimeout = 300ms;
|
||||
static_assert(ProcessEventsTimeout > GenericRescanInterval);
|
||||
|
||||
namespace libtransmission
|
||||
{
|
||||
@@ -52,15 +54,15 @@ class WatchDirTest
|
||||
{
|
||||
private:
|
||||
std::shared_ptr<struct event_base> ev_base_;
|
||||
std::unique_ptr<libtransmission::TimerMaker> timer_maker_;
|
||||
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
SandboxedTest::SetUp();
|
||||
ev_base_.reset(event_base_new(), event_base_free);
|
||||
|
||||
// speed up generic implementation
|
||||
tr_watchdir_generic_interval = OneHundredMsec;
|
||||
timer_maker_ = std::make_unique<libtransmission::EvTimerMaker>(ev_base_.get());
|
||||
Watchdir::setGenericRescanInterval(GenericRescanInterval);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
@@ -70,78 +72,60 @@ protected:
|
||||
SandboxedTest::TearDown();
|
||||
}
|
||||
|
||||
auto createWatchDir(std::string const& path, tr_watchdir_cb cb, void* cb_data)
|
||||
auto createWatchDir(std::string_view path, Watchdir::Callback callback)
|
||||
{
|
||||
auto const force_generic = GetParam() == WatchMode::GENERIC;
|
||||
return tr_watchdir_new(path.c_str(), cb, cb_data, ev_base_.get(), force_generic);
|
||||
auto watchdir = force_generic ?
|
||||
Watchdir::createGeneric(path, std::move(callback), *timer_maker_, GenericRescanInterval) :
|
||||
Watchdir::create(path, std::move(callback), *timer_maker_, ev_base_.get());
|
||||
dynamic_cast<impl::BaseWatchdir*>(watchdir.get())->setRetryDuration(RetryDuration);
|
||||
return watchdir;
|
||||
}
|
||||
|
||||
std::string createFile(std::string const& parent_dir, std::string const& name)
|
||||
void createFile(std::string_view dirname, std::string_view basename, std::string_view contents = ""sv)
|
||||
{
|
||||
auto path = parent_dir;
|
||||
path += TR_PATH_DELIMITER;
|
||||
path += name;
|
||||
|
||||
createFileWithContents(path, "");
|
||||
|
||||
return path;
|
||||
createFileWithContents(tr_pathbuf{ dirname, '/', basename }, contents);
|
||||
}
|
||||
|
||||
static std::string createDir(std::string const& parent_dir, std::string const& name)
|
||||
static std::string createDir(std::string_view dirname, std::string_view basename)
|
||||
{
|
||||
auto path = parent_dir;
|
||||
auto path = std::string{ dirname };
|
||||
path += TR_PATH_DELIMITER;
|
||||
path += name;
|
||||
path += basename;
|
||||
|
||||
tr_sys_dir_create(path, 0, 0700);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
void processEvents()
|
||||
void processEvents(std::chrono::milliseconds wait_interval = ProcessEventsTimeout)
|
||||
{
|
||||
event_base_loopexit(ev_base_.get(), &TwoHundredMsec);
|
||||
auto tv = timeval{};
|
||||
auto const seconds = std::chrono::duration_cast<std::chrono::seconds>(wait_interval);
|
||||
tv.tv_sec = static_cast<decltype(tv.tv_sec)>(seconds.count());
|
||||
|
||||
wait_interval -= seconds;
|
||||
auto const usec = std::chrono::duration_cast<std::chrono::microseconds>(wait_interval);
|
||||
tv.tv_usec = static_cast<decltype(tv.tv_usec)>(usec.count());
|
||||
|
||||
event_base_loopexit(ev_base_.get(), &tv);
|
||||
event_base_dispatch(ev_base_.get());
|
||||
}
|
||||
|
||||
struct CallbackData
|
||||
{
|
||||
explicit CallbackData(tr_watchdir_status status = TR_WATCHDIR_ACCEPT)
|
||||
: result{ status }
|
||||
{
|
||||
}
|
||||
tr_watchdir_status result{};
|
||||
|
||||
tr_watchdir_t wd = {};
|
||||
std::string name = {};
|
||||
};
|
||||
|
||||
static tr_watchdir_status callback(tr_watchdir_t wd, char const* name, void* vdata) noexcept
|
||||
{
|
||||
auto* data = static_cast<CallbackData*>(vdata);
|
||||
auto const result = data->result;
|
||||
|
||||
if (result != TR_WATCHDIR_RETRY)
|
||||
{
|
||||
data->wd = wd;
|
||||
data->name = name;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_P(WatchDirTest, construct)
|
||||
{
|
||||
auto const path = sandboxDir();
|
||||
|
||||
auto wd = createWatchDir(path, &callback, nullptr);
|
||||
EXPECT_NE(nullptr, wd);
|
||||
EXPECT_TRUE(tr_sys_path_is_same(path.c_str(), tr_watchdir_get_path(wd)));
|
||||
auto callback = [](std::string_view /*dirname*/, std::string_view /*basename*/)
|
||||
{
|
||||
return Watchdir::Action::Done;
|
||||
};
|
||||
auto watchdir = createWatchDir(path, callback);
|
||||
EXPECT_TRUE(watchdir);
|
||||
EXPECT_EQ(path, watchdir->dirname());
|
||||
|
||||
processEvents();
|
||||
|
||||
tr_watchdir_free(wd);
|
||||
}
|
||||
|
||||
TEST_P(WatchDirTest, initialScan)
|
||||
@@ -151,214 +135,114 @@ TEST_P(WatchDirTest, initialScan)
|
||||
// setup: start with an empty directory.
|
||||
// this block confirms that it's empty
|
||||
{
|
||||
auto wd_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
auto wd = createWatchDir(path, &callback, &wd_data);
|
||||
EXPECT_NE(nullptr, wd);
|
||||
|
||||
auto called = bool{ false };
|
||||
auto callback = [&called](std::string_view /*dirname*/, std::string_view /*basename*/)
|
||||
{
|
||||
called = true;
|
||||
return Watchdir::Action::Done;
|
||||
};
|
||||
auto watchdir = createWatchDir(path, callback);
|
||||
EXPECT_TRUE(watchdir);
|
||||
processEvents();
|
||||
EXPECT_EQ(nullptr, wd_data.wd);
|
||||
EXPECT_EQ("", wd_data.name);
|
||||
|
||||
tr_watchdir_free(wd);
|
||||
EXPECT_FALSE(called);
|
||||
}
|
||||
|
||||
// add a file
|
||||
auto const base_name = std::string{ "test.txt" };
|
||||
auto const base_name = "test.txt"sv;
|
||||
createFile(path, base_name);
|
||||
|
||||
// confirm that a wd will pick up the file that
|
||||
// was created before the wd was instantiated
|
||||
{
|
||||
auto wd_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
auto wd = createWatchDir(path, &callback, &wd_data);
|
||||
EXPECT_NE(nullptr, wd);
|
||||
|
||||
auto names = std::set<std::string>{};
|
||||
auto callback = [&names](std::string_view /*dirname*/, std::string_view basename)
|
||||
{
|
||||
names.insert(std::string{ basename });
|
||||
return Watchdir::Action::Done;
|
||||
};
|
||||
auto watchdir = createWatchDir(path, callback);
|
||||
EXPECT_TRUE(watchdir);
|
||||
processEvents();
|
||||
EXPECT_EQ(wd, wd_data.wd);
|
||||
EXPECT_EQ(base_name, wd_data.name);
|
||||
|
||||
tr_watchdir_free(wd);
|
||||
EXPECT_EQ(1U, std::size(names));
|
||||
EXPECT_EQ(base_name, *names.begin());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(WatchDirTest, watch)
|
||||
{
|
||||
auto const path = sandboxDir();
|
||||
auto const dirname = sandboxDir();
|
||||
|
||||
// create a new watchdir and confirm it's empty
|
||||
auto wd_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
auto wd = createWatchDir(path, &callback, &wd_data);
|
||||
EXPECT_NE(nullptr, wd);
|
||||
auto names = std::vector<std::string>{};
|
||||
auto callback = [&names](std::string_view /*dirname*/, std::string_view basename)
|
||||
{
|
||||
names.emplace_back(std::string{ basename });
|
||||
return Watchdir::Action::Done;
|
||||
};
|
||||
auto watchdir = createWatchDir(dirname, callback);
|
||||
processEvents();
|
||||
EXPECT_EQ(nullptr, wd_data.wd);
|
||||
EXPECT_EQ("", wd_data.name);
|
||||
EXPECT_TRUE(watchdir);
|
||||
EXPECT_TRUE(std::empty(names));
|
||||
|
||||
// test that a new file in an empty directory shows up
|
||||
auto const file1 = std::string{ "test1" };
|
||||
createFile(path, file1);
|
||||
auto const file1 = "test1"sv;
|
||||
createFile(dirname, file1);
|
||||
processEvents();
|
||||
EXPECT_EQ(wd, wd_data.wd);
|
||||
EXPECT_EQ(file1, wd_data.name);
|
||||
EXPECT_EQ(1U, std::size(names));
|
||||
if (!std::empty(names))
|
||||
{
|
||||
EXPECT_EQ(file1, names.front());
|
||||
}
|
||||
|
||||
// test that a new file in a nonempty directory shows up
|
||||
wd_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
auto const file2 = std::string{ "test2" };
|
||||
createFile(path, file2);
|
||||
names.clear();
|
||||
auto const file2 = "test2"sv;
|
||||
createFile(dirname, file2);
|
||||
processEvents();
|
||||
EXPECT_EQ(wd, wd_data.wd);
|
||||
EXPECT_EQ(file2, wd_data.name);
|
||||
processEvents();
|
||||
EXPECT_EQ(1U, std::size(names));
|
||||
if (!std::empty(names))
|
||||
{
|
||||
EXPECT_EQ(file2, names.front());
|
||||
}
|
||||
|
||||
// test that folders don't trigger the callback
|
||||
wd_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
createDir(path, "test3");
|
||||
names.clear();
|
||||
createDir(dirname, "test3"sv);
|
||||
processEvents();
|
||||
EXPECT_EQ(nullptr, wd_data.wd);
|
||||
EXPECT_EQ("", wd_data.name);
|
||||
|
||||
// cleanup
|
||||
tr_watchdir_free(wd);
|
||||
}
|
||||
|
||||
TEST_P(WatchDirTest, watchTwoDirs)
|
||||
{
|
||||
auto top = sandboxDir();
|
||||
|
||||
// create two empty directories and watch them
|
||||
auto wd1_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
auto const dir1 = createDir(top, "a");
|
||||
auto wd1 = createWatchDir(dir1, &callback, &wd1_data);
|
||||
EXPECT_NE(wd1, nullptr);
|
||||
auto wd2_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
auto const dir2 = createDir(top, "b");
|
||||
auto wd2 = createWatchDir(dir2, &callback, &wd2_data);
|
||||
EXPECT_NE(wd2, nullptr);
|
||||
|
||||
processEvents();
|
||||
EXPECT_EQ(nullptr, wd1_data.wd);
|
||||
EXPECT_EQ("", wd1_data.name);
|
||||
EXPECT_EQ(nullptr, wd2_data.wd);
|
||||
EXPECT_EQ("", wd2_data.name);
|
||||
|
||||
// add a file into directory 1 and confirm it triggers
|
||||
// a callback with the right wd
|
||||
auto const file1 = std::string{ "test.txt" };
|
||||
createFile(dir1, file1);
|
||||
processEvents();
|
||||
EXPECT_EQ(wd1, wd1_data.wd);
|
||||
EXPECT_EQ(file1, wd1_data.name);
|
||||
EXPECT_EQ(nullptr, wd2_data.wd);
|
||||
EXPECT_EQ("", wd2_data.name);
|
||||
|
||||
// add a file into directory 2 and confirm it triggers
|
||||
// a callback with the right wd
|
||||
wd1_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
wd2_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
auto const file2 = std::string{ "test2.txt" };
|
||||
createFile(dir2, file2);
|
||||
processEvents();
|
||||
EXPECT_EQ(nullptr, wd1_data.wd);
|
||||
EXPECT_EQ("", wd1_data.name);
|
||||
EXPECT_EQ(wd2, wd2_data.wd);
|
||||
EXPECT_EQ(file2, wd2_data.name);
|
||||
|
||||
// TODO(ckerr): watchdir.c seems to treat IGNORE and ACCEPT identically
|
||||
// so I'm not sure what's intended or what this is supposed to
|
||||
// be testing.
|
||||
wd1_data = CallbackData(TR_WATCHDIR_IGNORE);
|
||||
wd2_data = CallbackData(TR_WATCHDIR_IGNORE);
|
||||
auto const file3 = std::string{ "test3.txt" };
|
||||
auto const file4 = std::string{ "test4.txt" };
|
||||
createFile(dir1, file3);
|
||||
createFile(dir2, file4);
|
||||
processEvents();
|
||||
EXPECT_EQ(wd1, wd1_data.wd);
|
||||
EXPECT_EQ(file3, wd1_data.name);
|
||||
EXPECT_EQ(wd2, wd2_data.wd);
|
||||
EXPECT_EQ(file4, wd2_data.name);
|
||||
|
||||
// confirm that callbacks don't get confused
|
||||
// when there's a new file in directory 'a'
|
||||
// and a new directory in directory 'b'
|
||||
wd1_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
wd2_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
auto const file5 = std::string{ "test5.txt" };
|
||||
createFile(dir1, file5);
|
||||
createDir(dir2, file5);
|
||||
processEvents();
|
||||
EXPECT_EQ(wd1, wd1_data.wd);
|
||||
EXPECT_EQ(file5, wd1_data.name);
|
||||
EXPECT_EQ(nullptr, wd2_data.wd);
|
||||
EXPECT_EQ("", wd2_data.name);
|
||||
|
||||
// reverse the order of the previous test:
|
||||
// confirm that callbacks don't get confused
|
||||
// when there's a new file in directory 'b'
|
||||
// and a new directory in directory 'a'
|
||||
wd1_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
wd2_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
auto const file6 = std::string{ "test6.txt" };
|
||||
createDir(dir1, file6);
|
||||
createFile(dir2, file6);
|
||||
processEvents();
|
||||
EXPECT_EQ(nullptr, wd1_data.wd);
|
||||
EXPECT_EQ("", wd1_data.name);
|
||||
EXPECT_EQ(wd2, wd2_data.wd);
|
||||
EXPECT_EQ(file6, wd2_data.name);
|
||||
|
||||
// confirm that creating new directories in BOTH
|
||||
// watchdirs still triggers no callbacks
|
||||
wd1_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
wd2_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
auto const file7 = std::string{ "test7.txt" };
|
||||
auto const file8 = std::string{ "test8.txt" };
|
||||
createDir(dir1, file7);
|
||||
createDir(dir2, file8);
|
||||
processEvents();
|
||||
EXPECT_EQ(nullptr, wd1_data.wd);
|
||||
EXPECT_EQ("", wd1_data.name);
|
||||
EXPECT_EQ(nullptr, wd2_data.wd);
|
||||
EXPECT_EQ("", wd2_data.name);
|
||||
|
||||
// cleanup
|
||||
tr_watchdir_free(wd2);
|
||||
tr_watchdir_free(wd1);
|
||||
EXPECT_TRUE(std::empty(names));
|
||||
}
|
||||
|
||||
TEST_P(WatchDirTest, retry)
|
||||
{
|
||||
auto const path = sandboxDir();
|
||||
|
||||
// tune retry logic
|
||||
tr_watchdir_retry_limit = 10;
|
||||
tr_watchdir_retry_start_interval = FiftyMsec;
|
||||
tr_watchdir_retry_max_interval = tr_watchdir_retry_start_interval;
|
||||
|
||||
// test setup:
|
||||
// Start watching the test directory.
|
||||
// Create a file and return 'retry' back to the watchdir code
|
||||
// from our callback. This should cause the wd to wait a bit
|
||||
// and try again.
|
||||
auto wd_data = CallbackData(TR_WATCHDIR_RETRY);
|
||||
auto wd = createWatchDir(path, &callback, &wd_data);
|
||||
EXPECT_NE(nullptr, wd);
|
||||
processEvents();
|
||||
EXPECT_EQ(nullptr, wd_data.wd);
|
||||
EXPECT_EQ("", wd_data.name);
|
||||
// Create a file and return 'retry' back to the watchdir code from our callback.
|
||||
// This should cause the wd to wait a bit and try again.
|
||||
auto names = std::vector<std::string>{};
|
||||
auto callback = [&names](std::string_view /*dirname*/, std::string_view basename)
|
||||
{
|
||||
names.emplace_back(std::string{ basename });
|
||||
return Watchdir::Action::Retry;
|
||||
};
|
||||
auto watchdir = createWatchDir(path, callback);
|
||||
auto constexpr FastRetryWaitTime = 20ms;
|
||||
auto constexpr ThreeRetries = FastRetryWaitTime * 4;
|
||||
dynamic_cast<impl::BaseWatchdir*>(watchdir.get())->setRetryDuration(FastRetryWaitTime);
|
||||
|
||||
auto const test_file = std::string{ "test" };
|
||||
processEvents(ThreeRetries);
|
||||
EXPECT_EQ(0U, std::size(names));
|
||||
|
||||
auto const test_file = "test.txt"sv;
|
||||
createFile(path, test_file);
|
||||
processEvents();
|
||||
EXPECT_EQ(nullptr, wd_data.wd);
|
||||
EXPECT_EQ("", wd_data.name);
|
||||
|
||||
// confirm that wd retries.
|
||||
// return 'accept' in the callback so it won't keep retrying.
|
||||
wd_data = CallbackData(TR_WATCHDIR_ACCEPT);
|
||||
processEvents();
|
||||
EXPECT_EQ(wd, wd_data.wd);
|
||||
EXPECT_EQ(test_file, wd_data.name);
|
||||
|
||||
tr_watchdir_free(wd);
|
||||
processEvents(ThreeRetries);
|
||||
EXPECT_LE(2, std::size(names));
|
||||
for (auto const& name : names)
|
||||
{
|
||||
EXPECT_EQ(test_file, name);
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P( //
|
||||
|
||||
Reference in New Issue
Block a user