Files
syncspirit/tests/088-watcher.cpp

1686 lines
81 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 Ivan Baidakou
#include "syncspirit-config.h"
#include "test-utils.h"
#if defined(SYNCSPIRIT_WATCHER_ANY)
#include "access.h"
#include "test-watcher.h"
#include "fs/fs_slave.h"
#include "fs/fs_context.h"
#include "fs/fs_supervisor.h"
#include "fs/watcher_actor.h"
#include "fs/utils.h"
#include "fs/task/scan_dir.h"
#include "utils/error_code.h"
#include "net/names.h"
#include <deque>
#include <boost/nowide/convert.hpp>
#include <stdexcept>
#include <catch2/generators/catch_generators.hpp>
using namespace syncspirit;
using namespace syncspirit::test;
using namespace syncspirit::model;
using namespace syncspirit::fs;
namespace bfs = std::filesystem;
using boost::nowide::narrow;
struct fixture_t;
namespace native {
void rename(const bfs::path &from, const bfs::path &to) {
#ifndef SYNCSPIRIT_WIN
bfs::rename(from, to);
#else
auto from_native = from.native().data();
auto to_native = to.native().data();
if (!MoveFileExW(from_native, to_native, MOVEFILE_WRITE_THROUGH)) {
auto ec = sys::error_code(::GetLastError(), sys::system_category());
REQUIRE(ec.message() == "");
}
#endif
}
} // namespace native
static const auto RETENSION_TIMEOUT = r::pt::millisec{1};
static const auto TIMEOUT = r::pt::millisec{10};
struct supervisor_t : fs::fs_supervisor_t {
using parent_t = fs::fs_supervisor_t;
using parent_t::parent_t;
void configure(r::plugin::plugin_base_t &plugin) noexcept override {
parent_t::configure(plugin);
plugin.with_casted<r::plugin::registry_plugin_t>(
[&](auto &p) { p.register_name(net::names::coordinator, get_address()); });
plugin.with_casted<r::plugin::starter_plugin_t>([&](auto &p) {
p.subscribe_actor(&supervisor_t::on_watch);
p.subscribe_actor(&supervisor_t::on_unwatch);
p.subscribe_actor(&supervisor_t::on_changes);
});
}
void launch_children() noexcept override {
// NOOP
}
void on_watch(fs::message::watch_folder_t &) noexcept;
void on_unwatch(fs::message::unwatch_folder_t &) noexcept;
void on_changes(fs::message::folder_changes_t &) noexcept;
fixture_t *fixture;
};
enum class poll_t { single, trigger_timer };
struct fixture_t {
using target_ptr_t = r::intrusive_ptr_t<fs::platform::watcher_base_t>;
using fs_context_ptr_r = r::intrusive_ptr_t<fs::fs_context_t>;
using change_message_ptr_t = r::intrusive_ptr_t<fs::message::folder_changes_t>;
using change_messages_t = std::deque<change_message_ptr_t>;
fixture_t(bool auto_launch_ = true) noexcept
: auto_launch{auto_launch_}, root_path{unique_path()}, path_guard{root_path} {
bfs::create_directory(root_path);
log = utils::get_logger("fixture");
}
void run() noexcept {
fs_context.reset(new fs::fs_context_t(timeout * 2));
sup = fs_context->create_supervisor<supervisor_t>().timeout(timeout).create_registry().finish();
sup->fixture = this;
sup->start();
sup->do_process();
REQUIRE(static_cast<r::actor_base_t *>(sup.get())->access<to::state>() == r::state_t::OPERATIONAL);
updates_mediator = new fs::updates_mediator_t(retension() * 2);
watched_folders.reset(new watched_folders_t());
if (auto_launch) {
launch_target();
REQUIRE(static_cast<r::actor_base_t *>(target.get())->access<to::state>() == r::state_t::OPERATIONAL);
}
fs_context->update_time();
main();
sup->do_process();
sup->do_shutdown();
sup->do_process();
CHECK(static_cast<r::actor_base_t *>(sup.get())->access<to::state>() == r::state_t::SHUT_DOWN);
}
virtual void launch_target() {
target = sup->create_actor<target_ptr_t::element_type>()
.timeout(timeout)
.change_retension(retension())
.updates_mediator(updates_mediator)
.watched_folders(watched_folders)
.finish();
sup->do_process();
}
void await_events(poll_t poll_type, size_t await_changes = 0, bool flatten = false) {
using clock_t = pt::microsec_clock;
changes.clear();
auto deadline = clock_t::local_time() + timeout * 10;
auto do_flatten_on_demand = [&]() {
if (flatten) {
auto copy = change_messages_t();
auto changes_comparator = [](const auto &l, const auto &r) -> bool {
return proto::get_name(l) < proto::get_name(r);
};
for (auto &m : changes) {
for (auto &folder_change : m->payload) {
auto file_changes = folder_change.file_changes;
std::sort(file_changes.begin(), file_changes.end(), changes_comparator);
for (auto &file_change : file_changes) {
auto solo_changes = fs::payload::folder_change_t{folder_change.folder_id, {file_change}};
auto msg = change_message_ptr_t();
msg.reset(new fs::message::folder_changes_t(sup->get_address(), std::move(solo_changes)));
copy.emplace_back(msg);
}
}
}
auto comparator = [&](const auto &lm, const auto &rm) -> bool {
auto l = lm->payload[0].file_changes[0];
auto r = rm->payload[0].file_changes[0];
return changes_comparator(l, r);
};
std::sort(copy.begin(), copy.end(), comparator);
changes = std::move(copy);
}
};
do {
auto prev_sz = changes.size();
auto has_events = fs_context->wait_next_event();
fs_context->update_time();
LOG_TRACE(log, "updating time... already has {} changes", changes.size());
sup->do_process();
auto wait_next = (changes.size() < await_changes) || (!has_events && poll_type == poll_t::trigger_timer);
if (wait_next) {
fs_context->wait_next_event();
fs_context->update_time();
sup->do_process();
}
do_flatten_on_demand();
} while ((changes.size() < await_changes) && clock_t::local_time() < deadline);
do_flatten_on_demand();
REQUIRE(changes.size() >= await_changes);
}
virtual void on_watch(fs::message::watch_folder_t &msg) noexcept {
CHECK(!msg.payload.ec);
CHECK(msg.payload.ec.message() != "");
++watched_replies;
}
virtual void on_unwatch(fs::message::unwatch_folder_t &msg) noexcept {
CHECK(!msg.payload.ec);
CHECK(msg.payload.ec.message() != "");
++unwatched_replies;
}
virtual void on_changes(fs::message::folder_changes_t &msg) noexcept { changes.emplace_back(&msg); }
virtual void main() noexcept {}
r::pt::time_duration retension() { return RETENSION_TIMEOUT; }
bool auto_launch;
bfs::path root_path;
test::path_guard_t path_guard;
fs_context_ptr_r fs_context;
r::intrusive_ptr_t<supervisor_t> sup;
fs::updates_mediator_ptr_t updates_mediator;
fs::watched_folders_ptr_t watched_folders;
target_ptr_t target;
r::pt::time_duration timeout = TIMEOUT;
change_messages_t changes;
size_t watched_replies = 0;
size_t unwatched_replies = 0;
utils::logger_t log;
};
void supervisor_t::on_watch(fs::message::watch_folder_t &msg) noexcept { fixture->on_watch(msg); }
void supervisor_t::on_unwatch(fs::message::unwatch_folder_t &msg) noexcept { fixture->on_unwatch(msg); }
void supervisor_t::on_changes(fs::message::folder_changes_t &msg) noexcept { fixture->on_changes(msg); }
void test_watcher_base() {
struct F : fixture_t {
using fixture_t::fixture_t;
void launch_target() override {
target = sup->create_actor<test::test_watcher_t>()
.timeout(timeout)
.change_retension(retension())
.updates_mediator(updates_mediator)
.watched_folders(watched_folders)
.finish();
sup->do_process();
}
void main() noexcept override {
using U = fs::update_type_t;
auto folder_id = std::string("my-folder-id");
auto back_addr = sup->get_address();
sup->route<fs::payload::watch_folder_t>(target->get_address(), back_addr, root_path, folder_id);
sup->do_process();
REQUIRE(watched_replies == 1);
auto deadline = r::pt::microsec_clock::local_time() + retension();
SECTION("simple (creation)") {
SECTION("dir") {
auto own_name = bfs::path(L"папка");
auto sub_path = root_path / own_name;
auto requires_refinement = GENERATE(true, false);
bfs::create_directories(sub_path);
target->push(deadline, folder_id, narrow(own_name.wstring()), {}, U::created, requires_refinement);
await_events(poll_t::single, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(own_name.wstring()));
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_type(file_change) == proto::FileInfoType::DIRECTORY);
CHECK(proto::get_permissions(file_change));
CHECK(file_change.requires_refinement == requires_refinement);
}
SECTION("file") {
auto own_name = bfs::path(L"файл.bin");
auto sub_path = root_path / own_name;
write_file(sub_path, "12345");
target->push(deadline, folder_id, narrow(own_name.wstring()), {}, U::created, false);
await_events(poll_t::single, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(own_name.wstring()));
CHECK(proto::get_size(file_change) == 5);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(file_change));
}
#ifndef SYNCSPIRIT_WIN
SECTION("symlink") {
auto own_name = bfs::path(L"ссылка");
auto sub_path = root_path / own_name;
auto where = bfs::path("/to/some/where");
bfs::create_symlink(where, sub_path);
target->push(deadline, folder_id, narrow(own_name.wstring()), {}, U::created, false);
await_events(poll_t::single, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(own_name.wstring()));
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_type(file_change) == proto::FileInfoType::SYMLINK);
CHECK(proto::get_permissions(file_change));
CHECK(proto::get_symlink_target(file_change) == narrow(where.wstring()));
}
#endif
}
SECTION("file moving") {
auto name_1 = bfs::path(L"файл-1.bin");
auto name_2 = bfs::path(L"файл-2.bin");
auto sub_path_1 = root_path / name_1;
auto sub_path_2 = root_path / name_2;
auto path_2_str = narrow(sub_path_2.generic_wstring());
write_file(sub_path_1, "12345");
target->push(deadline, folder_id, narrow(name_1.wstring()), path_2_str, U::meta, false);
await_events(poll_t::single, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(name_1.wstring()));
CHECK(proto::get_size(file_change) == 5);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(file_change));
CHECK(file_change.prev_path == path_2_str);
CHECK(file_change.update_reason == update_type_t::meta);
}
SECTION("changes accumulation") {
auto deadline_2 = deadline + retension();
SECTION("simple case") {
auto own_name = bfs::path(L"файл.bin");
auto sub_path = root_path / own_name;
write_file(sub_path, "12345");
target->push(deadline, folder_id, narrow(own_name.wstring()), {}, U::created, false);
target->push(deadline_2, folder_id, narrow(own_name.wstring()), {}, U::content, false);
await_events(poll_t::single);
REQUIRE(changes.size() == 0);
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(own_name.wstring()));
CHECK(proto::get_size(file_change) == 5);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(file_change));
CHECK(file_change.update_reason == update_type_t::created);
}
SECTION("create , delete -> collapse to void") {
auto own_name = bfs::path(L"файл.bin");
auto sub_path = root_path / own_name;
write_file(sub_path, "12345");
target->push(deadline, folder_id, narrow(own_name.wstring()), {}, U::created, false);
target->push(deadline_2, folder_id, narrow(own_name.wstring()), {}, U::deleted, false);
await_events(poll_t::trigger_timer);
REQUIRE(changes.size() == 0);
}
SECTION("content , meta -> collapse to content") {
auto own_name = bfs::path(L"файл.bin");
auto sub_path = root_path / own_name;
write_file(sub_path, "12345");
target->push(deadline, folder_id, narrow(own_name.wstring()), {}, U::content, false);
target->push(deadline_2, folder_id, narrow(own_name.wstring()), {}, U::meta, false);
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(own_name.wstring()));
CHECK(proto::get_size(file_change) == 5);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(file_change));
CHECK(file_change.update_reason == update_type_t::content);
}
SECTION("mv(a, b), content(b) -> rm(a), create(b)") {
auto name_1 = bfs::path(L"файл-1.bin");
auto name_2 = bfs::path(L"файл-2.bin");
auto name_1_str = narrow(name_1.generic_wstring());
auto name_2_str = narrow(name_2.generic_wstring());
write_file(root_path / name_2, "12345");
target->push(deadline, folder_id, name_2_str, name_1_str, U::meta, false);
target->push(deadline, folder_id, name_2_str, {}, U::content, false);
await_events(poll_t::trigger_timer, 2, true);
auto &payload_1 = changes[0]->payload;
REQUIRE(payload_1.size() == 1);
auto &folder_change_1 = payload_1[0];
REQUIRE(folder_change_1.folder_id == folder_id);
REQUIRE(folder_change_1.file_changes.size() == 1);
auto &change_0 = folder_change_1.file_changes[0];
CHECK(proto::get_name(change_0) == narrow(name_1.wstring()));
CHECK(proto::get_size(change_0) == 0);
CHECK(proto::get_type(change_0) == proto::FileInfoType::FILE);
CHECK(change_0.update_reason == update_type_t::deleted);
CHECK(change_0.prev_path == "");
auto &payload_2 = changes[1]->payload;
REQUIRE(payload_2.size() == 1);
auto &folder_change_2 = payload_2[0];
auto &change_1 = folder_change_2.file_changes[0];
CHECK(proto::get_name(change_1) == narrow(name_2.wstring()));
CHECK(proto::get_size(change_1) == 5);
CHECK(proto::get_type(change_1) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(change_1));
CHECK(change_1.update_reason == update_type_t::created);
CHECK(change_1.prev_path == "");
}
SECTION("mv(a, b), content(b), meta(b) -> rm(a), create(b)") {
auto name_1 = bfs::path(L"файл-1.bin");
auto name_2 = bfs::path(L"файл-2.bin");
auto name_1_str = narrow(name_1.generic_wstring());
auto name_2_str = narrow(name_2.generic_wstring());
write_file(root_path / name_2, "12345");
target->push(deadline, folder_id, name_2_str, name_1_str, U::meta, false);
target->push(deadline_2, folder_id, name_2_str, {}, U::content, false);
target->push(deadline_2, folder_id, name_2_str, {}, U::meta, false);
await_events(poll_t::trigger_timer, 2, true);
auto &payload_1 = changes[0]->payload;
REQUIRE(payload_1.size() == 1);
auto &folder_change_1 = payload_1[0];
REQUIRE(folder_change_1.folder_id == folder_id);
REQUIRE(folder_change_1.file_changes.size() == 1);
auto &change_0 = folder_change_1.file_changes[0];
CHECK(proto::get_name(change_0) == narrow(name_1.wstring()));
CHECK(proto::get_size(change_0) == 0);
CHECK(proto::get_type(change_0) == proto::FileInfoType::FILE);
CHECK(change_0.update_reason == update_type_t::deleted);
CHECK(change_0.prev_path == "");
auto &payload_2 = changes[1]->payload;
REQUIRE(payload_2.size() == 1);
auto &folder_change_2 = payload_2[0];
auto &change_1 = folder_change_2.file_changes[0];
CHECK(proto::get_name(change_1) == narrow(name_2.wstring()));
CHECK(proto::get_size(change_1) == 5);
CHECK(proto::get_type(change_1) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(change_1));
CHECK(change_1.update_reason == update_type_t::created);
CHECK(change_1.prev_path == "");
}
SECTION("content change + rename => remove + content change") {
auto name_1 = bfs::path(L"файл-1.bin");
auto name_2 = bfs::path(L"файл-2.bin");
auto name_1_str = narrow(name_1.generic_wstring());
auto name_2_str = narrow(name_2.generic_wstring());
write_file(root_path / name_2, "12345");
target->push(deadline, folder_id, name_2_str, {}, U::content, false);
target->push(deadline, folder_id, name_2_str, name_1_str, U::meta, false);
await_events(poll_t::trigger_timer, 1, true);
auto &payload_1 = changes[0]->payload;
REQUIRE(payload_1.size() == 1);
auto &folder_change_1 = payload_1[0];
REQUIRE(folder_change_1.folder_id == folder_id);
REQUIRE(folder_change_1.file_changes.size() == 1);
auto &change_0 = folder_change_1.file_changes[0];
CHECK(proto::get_name(change_0) == narrow(name_1.wstring()));
CHECK(proto::get_size(change_0) == 0);
CHECK(proto::get_type(change_0) == proto::FileInfoType::FILE);
CHECK(change_0.update_reason == update_type_t::deleted);
CHECK(change_0.prev_path == "");
auto &payload_2 = changes[1]->payload;
REQUIRE(payload_2.size() == 1);
auto &folder_change_2 = payload_2[0];
auto &change_1 = folder_change_2.file_changes[0];
CHECK(proto::get_name(change_1) == narrow(name_2.wstring()));
CHECK(proto::get_size(change_1) == 5);
CHECK(proto::get_type(change_1) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(change_1));
CHECK(change_1.update_reason == update_type_t::created);
CHECK(change_1.prev_path == "");
}
SECTION("move, delete -> collapse to delete of original") {
auto name_1 = bfs::path(L"файл-1.bin");
auto name_2 = bfs::path(L"файл-2.bin");
auto name_1_str = narrow(name_1.generic_wstring());
auto name_2_str = narrow(name_2.generic_wstring());
target->push(deadline, folder_id, name_2_str, name_1_str, U::meta, false);
target->push(deadline_2, folder_id, name_2_str, {}, U::deleted, false);
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == name_1_str);
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(proto::get_deleted(file_change));
CHECK(file_change.update_reason == update_type_t::deleted);
CHECK(file_change.prev_path.empty());
}
SECTION("mv(a, b), mv(b, c) -> collapse to mv(a, c") {
auto name_1 = bfs::path(L"файл-1.bin");
auto name_2 = bfs::path(L"файл-2.bin");
auto name_3 = bfs::path(L"файл-3.bin");
auto name_1_str = narrow(name_1.generic_wstring());
auto name_2_str = narrow(name_2.generic_wstring());
auto name_3_str = narrow(name_3.generic_wstring());
write_file(root_path / name_3, "12345");
target->push(deadline, folder_id, name_2_str, name_1_str, U::meta, false);
target->push(deadline_2, folder_id, name_3_str, name_2_str, U::meta, false);
await_events(poll_t::trigger_timer);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == name_3_str);
CHECK(proto::get_size(file_change) == 5);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(file_change));
CHECK(file_change.update_reason == update_type_t::meta);
CHECK(file_change.prev_path == name_1_str);
}
SECTION("mv(a, b) -> mv(b, a) -> noop") {
auto name_1 = bfs::path(L"файл-1.bin");
auto name_2 = bfs::path(L"файл-2.bin");
auto name_1_str = narrow(name_1.generic_wstring());
auto name_2_str = narrow(name_2.generic_wstring());
write_file(root_path / name_1, "12345");
target->push(deadline, folder_id, name_2_str, name_1_str, U::meta, false);
target->push(deadline_2, folder_id, name_1_str, name_2_str, U::meta, false);
await_events(poll_t::trigger_timer);
REQUIRE(changes.size() == 0);
}
SECTION("change(a), mv(a, b) -> rm(a), create(b)") {
auto name_1 = bfs::path(L"файл-1.bin");
auto name_2 = bfs::path(L"файл-2.bin");
auto name_1_str = narrow(name_1.generic_wstring());
auto name_2_str = narrow(name_2.generic_wstring());
write_file(root_path / name_2, "12345");
target->push(deadline, folder_id, name_1_str, {}, U::content, false);
target->push(deadline, folder_id, name_2_str, name_1_str, U::meta, false);
await_events(poll_t::trigger_timer, 2, true);
auto &payload_1 = changes[0]->payload;
REQUIRE(payload_1.size() == 1);
auto &folder_change_1 = payload_1[0];
REQUIRE(folder_change_1.folder_id == folder_id);
REQUIRE(folder_change_1.file_changes.size() == 1);
auto &change_1 = folder_change_1.file_changes.front();
CHECK(change_1.update_reason == update_type_t::deleted);
CHECK(proto::get_name(change_1) == name_1_str);
CHECK(proto::get_size(change_1) == 0);
CHECK(proto::get_type(change_1) == proto::FileInfoType::FILE);
CHECK(change_1.prev_path.empty());
auto &payload_2 = changes[1]->payload;
REQUIRE(payload_2.size() == 1);
auto &folder_change_2 = payload_2[0];
auto &change_2 = folder_change_2.file_changes[0];
CHECK(proto::get_name(change_2) == narrow(name_2.wstring()));
CHECK(proto::get_size(change_2) == 5);
CHECK(proto::get_type(change_2) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(change_2));
CHECK(change_2.update_reason == update_type_t::created);
CHECK(change_2.prev_path == "");
}
}
SECTION("updates mediator") {
auto own_name = bfs::path(L"файл.bin");
auto sub_path = root_path / own_name;
write_file(sub_path, "12345");
updates_mediator->enable(true);
updates_mediator->mask(sub_path, {}, deadline);
target->push(deadline, folder_id, narrow(own_name.wstring()), {}, U::created, false);
await_events(poll_t::single);
REQUIRE(changes.size() == 0);
target->push(deadline, folder_id, narrow(own_name.wstring()), {}, U::created, false);
await_events(poll_t::single);
REQUIRE(changes.size() == 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(own_name.wstring()));
CHECK(proto::get_size(file_change) == 5);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(file_change));
}
}
};
F().run();
}
struct fixture_real_t : fixture_t {
using fixture_t::fixture_t;
void launch_target() override {
target = sup->create_actor<fs::watch_actor_t>()
.timeout(timeout)
.change_retension(retension())
.updates_mediator(updates_mediator)
.watched_folders(watched_folders)
.finish();
sup->do_process();
}
virtual bool notify_upon_watch() { return true; }
void recurse_scan(const bfs::path &path) {
#ifdef SYNCSPIRIT_WATCHER_UNIX
auto queue = std::list<bfs::path>{path};
auto dummy_slave = fs_slave_t();
auto ctx = execution_context_t();
auto watcher = static_cast<fs::platform::unix::watcher_t *>(target.get());
while (!queue.empty()) {
auto &p = queue.front();
auto task = fs::task::scan_dir_t(p, {}, {}, true, false, false);
task.process(dummy_slave, ctx);
watcher->notify(task);
for (auto &child : task.child_infos) {
if (child.status.type() == bfs::file_type::directory) {
queue.push_back(child.path);
}
}
queue.pop_front();
}
#endif
}
void watch_folder(std::string_view folder_id, const bfs::path &folder_path = {}) noexcept {
auto watched_path = folder_path.empty() ? root_path : folder_path;
auto back_addr = sup->get_address();
sup->route<fs::payload::watch_folder_t>(target->get_address(), back_addr, watched_path, folder_id);
sup->do_process();
REQUIRE(watched_replies == 1);
LOG_TRACE(log, "folder is being watched on '{}', updating time...", narrow(watched_path.wstring()));
fs_context->update_time();
if (notify_upon_watch()) {
recurse_scan(watched_path);
}
}
};
void test_start_n_shutdown() {
struct F : fixture_real_t {
using fixture_real_t::fixture_real_t;
void main() noexcept override {
launch_target();
CHECK(static_cast<r::actor_base_t *>(target.get())->access<to::state>() == r::state_t::OPERATIONAL);
REQUIRE(static_cast<r::actor_base_t *>(sup.get())->access<to::state>() == r::state_t::OPERATIONAL);
target->do_shutdown();
sup->do_process();
CHECK(static_cast<r::actor_base_t *>(target.get())->access<to::state>() == r::state_t::SHUT_DOWN);
REQUIRE(static_cast<r::actor_base_t *>(sup.get())->access<to::state>() == r::state_t::OPERATIONAL);
}
};
F(false).run();
}
void test_watch_unwatch() {
struct F : fixture_real_t {
using fixture_real_t::fixture_real_t;
void on_watch(fs::message::watch_folder_t &msg) noexcept override {
auto &counter = msg.payload.ec ? watched_errors : watched_successes;
++counter;
++watched_replies;
}
void on_unwatch(fs::message::unwatch_folder_t &msg) noexcept override {
auto &counter = msg.payload.ec ? unwatched_errors : unwatched_successes;
++counter;
++unwatched_replies;
}
void main() noexcept override {
auto folder_id = std::string("my-folder-id");
auto back_addr = sup->get_address();
sup->route<fs::payload::watch_folder_t>(target->get_address(), back_addr, root_path, folder_id);
sup->route<fs::payload::watch_folder_t>(target->get_address(), back_addr, root_path, folder_id);
sup->do_process();
CHECK(watched_replies == 2);
CHECK(watched_successes == 1);
CHECK(watched_errors == 1);
CHECK(unwatched_replies == 0);
CHECK(unwatched_successes == 0);
CHECK(unwatched_errors == 0);
sup->route<fs::payload::unwatch_folder_t>(target->get_address(), back_addr, folder_id);
sup->do_process();
CHECK(unwatched_replies == 1);
CHECK(unwatched_successes == 1);
CHECK(unwatched_errors == 0);
sup->route<fs::payload::unwatch_folder_t>(target->get_address(), back_addr, folder_id);
sup->do_process();
CHECK(unwatched_replies == 2);
CHECK(unwatched_successes == 1);
CHECK(unwatched_errors == 1);
}
int watched_successes = 0;
int watched_errors = 0;
int unwatched_successes = 0;
int unwatched_errors = 0;
};
F().run();
}
void test_tmp_ignoring() {
struct F : fixture_real_t {
using fixture_real_t::fixture_real_t;
void main() noexcept override {
auto folder_id = std::string("my-folder-id");
auto back_addr = sup->get_address();
bfs::create_directories(root_path / "prev-dir.syncspirit-tmp");
write_file(root_path / "01.syncspirit-tmp", "12345");
watch_folder(folder_id);
SECTION("ignoring") {
SECTION("create a dir") {
auto path = root_path / "my.syncspirit-tmp";
bfs::create_directories(path);
}
SECTION("rm file") { bfs::remove(root_path / "01.syncspirit-tmp"); }
SECTION("rm dir") { bfs::remove_all(root_path / "prev-dir.syncspirit-tmp"); }
SECTION("write file") { write_file(root_path / "my.syncspirit-tmp", "12345"); }
SECTION("rename file tmp -> tmp") {
bfs::rename(root_path / "01.syncspirit-tmp", root_path / "02.syncspirit-tmp");
}
#ifndef SYNCSPIRIT_WIN
SECTION("create symlink") {
bfs::create_symlink(bfs::path("xxx"), root_path / "my-link.syncspirit-tmp");
}
#endif
for (int i = 0; i < 2; ++i) {
fs_context->wait_next_event();
fs_context->update_time();
}
#ifdef SYNCSPIRIT_WATCHER_KQUEUE
sup->do_process();
CHECK(changes.size() == 1);
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == "");
CHECK(proto::get_type(file_change) == proto::FileInfoType::DIRECTORY);
CHECK(file_change.requires_refinement);
}
#else
sup->do_process();
CHECK(changes.size() == 0);
#endif
}
SECTION("not ignoring") {
bfs::rename(root_path / "01.syncspirit-tmp", root_path / "02.file");
await_events(poll_t::trigger_timer, 1);
}
}
};
F().run();
};
void test_real_impl() {
struct F : fixture_real_t {
using fixture_real_t::fixture_real_t;
void main() noexcept override {
auto folder_id = std::string("my-folder-id");
auto back_addr = sup->get_address();
SECTION("(create) new dir") {
#ifndef SYNCSPIRIT_WIN
auto expected_refinement = true;
#else
auto expected_refinement = false;
#endif
watch_folder(folder_id);
auto path = root_path / "my-dir";
bfs::create_directories(path);
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == "my-dir");
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_type(file_change) == proto::FileInfoType::DIRECTORY);
CHECK(proto::get_permissions(file_change));
CHECK(file_change.update_reason == update_type_t::created);
CHECK(file_change.requires_refinement == expected_refinement);
}
SECTION("(create with recursion) new dir + new file") {
watch_folder(folder_id);
#ifndef SYNCSPIRIT_WIN
auto expected_refinement = true;
#else
auto expected_refinement = false;
#endif
auto path_dir = root_path / "my-dir";
bfs::create_directories(path_dir);
await_events(poll_t::trigger_timer, 1);
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == "my-dir");
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_type(file_change) == proto::FileInfoType::DIRECTORY);
CHECK(proto::get_permissions(file_change));
CHECK(file_change.requires_refinement == expected_refinement);
changes.clear();
}
#ifndef SYNCSPIRIT_WIN
recurse_scan(root_path);
#endif
auto path_file = path_dir / L"файл.bin";
write_file(path_file, "12345");
await_events(poll_t::trigger_timer, 1);
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(L"my-dir/файл.bin"));
CHECK(proto::get_size(file_change) == 5);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(file_change));
CHECK(file_change.update_reason == update_type_t::created);
}
}
SECTION("(content change) file") {
auto path = root_path / "my-file";
write_file(path, "12345");
watch_folder(folder_id);
write_file(path, "123456");
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == "my-file");
CHECK(proto::get_size(file_change) == 6);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(file_change));
CHECK(file_change.update_reason == update_type_t::content);
CHECK(!file_change.requires_refinement);
}
SECTION("(delete) single file") {
auto path = root_path / "my-file";
write_file(path, "12345");
watch_folder(folder_id);
bfs::remove(path);
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == "my-file");
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(!file_change.requires_refinement);
CHECK(file_change.update_reason == update_type_t::deleted);
}
SECTION("move") {
auto subdir_path = root_path / "my-root";
auto a_path = subdir_path / L"a/b/п1";
auto x_path = subdir_path / L"x/y/п2";
bfs::create_directories(a_path);
bfs::create_directories(x_path);
SECTION("file inside root => meta") {
if (!test::wine_environment()) {
auto path_1 = subdir_path / L"my-file.1";
auto path_2 = subdir_path / L"my-file.2";
write_file(path_1, "12345");
watch_folder(folder_id);
SECTION("native::rename") { native::rename(path_1, path_2); }
SECTION("bfs::rename") { bfs::rename(path_1, path_2); }
#ifndef SYNCSPIRIT_WIN
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(L"my-root/my-file.2"));
CHECK(proto::get_size(file_change) == 5);
CHECK(!proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::meta);
CHECK(file_change.prev_path == narrow(L"my-root/my-file.1"));
#else
await_events(poll_t::trigger_timer, 2, true);
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes[0];
CHECK(proto::get_name(file_change) == narrow(L"my-root/my-file.1"));
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::deleted);
CHECK(file_change.prev_path == "");
changes.pop_front();
}
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes[0];
CHECK(proto::get_name(file_change) == narrow(L"my-root/my-file.2"));
CHECK(proto::get_size(file_change) == 5);
CHECK(!proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::created);
CHECK(file_change.requires_refinement);
CHECK(file_change.prev_path == "");
}
#endif
}
}
SECTION("dirs inside folder => meta") {
if (!test::wine_environment()) {
auto path_1 = subdir_path / L"папка1";
auto path_2 = subdir_path / L"папка2";
bfs::create_directories(path_1);
watch_folder(folder_id);
native::rename(path_1, path_2);
#ifndef SYNCSPIRIT_WIN
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(L"my-root/папка2"));
CHECK(proto::get_size(file_change) == 0);
CHECK(!proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::DIRECTORY);
CHECK(file_change.update_reason == update_type_t::meta);
CHECK(file_change.prev_path == narrow(L"my-root/папка1"));
#else
await_events(poll_t::trigger_timer, 2, true);
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes[0];
CHECK(proto::get_name(file_change) == narrow(L"my-root/папка1"));
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::deleted);
CHECK(file_change.prev_path == "");
changes.pop_front();
}
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes[0];
CHECK(proto::get_name(file_change) == narrow(L"my-root/папка2"));
CHECK(proto::get_size(file_change) == 0);
CHECK(!proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::DIRECTORY);
CHECK(file_change.update_reason == update_type_t::created);
CHECK(file_change.prev_path == "");
}
#endif
}
}
SECTION("outside of my dir => delete") {
if (!test::wine_environment()) {
auto path_1 = a_path / L"my-file.1";
auto path_2 = root_path / L"my-file.2";
write_file(path_1, "12345");
watch_folder(folder_id, subdir_path);
native::rename(path_1, path_2);
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(L"a/b/п1/my-file.1"));
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::deleted);
}
}
SECTION("into my dir => create") {
if (!test::wine_environment()) {
auto path_1 = root_path / L"my-file.1";
auto path_2 = x_path / L"my-file.2";
write_file(path_1, "12345");
watch_folder(folder_id, subdir_path);
native::rename(path_1, path_2);
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(L"x/y/п2/my-file.2"));
CHECK(proto::get_size(file_change) == 5);
CHECK(!proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::created);
CHECK(file_change.prev_path.empty());
}
}
SECTION("inside folder, cross-dir moving => meta") {
if (!test::wine_environment()) {
auto path_1 = a_path / L"my-file.1";
auto path_2 = x_path / L"my-file.2";
write_file(path_1, "12345");
watch_folder(folder_id, subdir_path);
native::rename(path_1, path_2);
#ifndef SYNCSPIRIT_WIN
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(L"x/y/п2/my-file.2"));
CHECK(proto::get_size(file_change) == 5);
CHECK(!proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::meta);
CHECK(file_change.prev_path == narrow(L"a/b/п1/my-file.1"));
#else
await_events(poll_t::trigger_timer, 2, true);
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == narrow(L"a/b/п1/my-file.1"));
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::deleted);
CHECK(file_change.prev_path == "");
changes.pop_front();
}
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
{
auto &file_change = folder_change.file_changes[0];
CHECK(proto::get_name(file_change) == narrow(L"x/y/п2/my-file.2"));
CHECK(proto::get_size(file_change) == 5);
CHECK(!proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::created);
CHECK(file_change.prev_path == "");
}
changes.pop_front();
}
#endif
}
}
}
#ifndef SYNCSPIRIT_WIN
SECTION("(permissions) file") {
auto path = root_path / "my-file";
write_file(path, "12345");
bfs::permissions(path, bfs::perms::owner_read);
watch_folder(folder_id);
bfs::permissions(path, bfs::perms::owner_write);
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == "my-file");
CHECK(proto::get_size(file_change) == 5);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(proto::get_permissions(file_change));
CHECK(file_change.update_reason == update_type_t::meta);
CHECK(!file_change.requires_refinement);
}
SECTION("(delete + create) symlink target change") {
auto path = root_path / "my-file";
auto link_target_1 = std::string_view("/some/where/1");
auto link_target_2 = std::string_view("/some/where/2");
bfs::create_symlink(bfs::path(link_target_1), path);
watch_folder(folder_id);
bfs::remove(path);
bfs::create_symlink(bfs::path(link_target_2), path);
await_events(poll_t::trigger_timer, 1);
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == "my-file");
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_type(file_change) == proto::FileInfoType::SYMLINK);
CHECK(proto::get_symlink_target(file_change) == link_target_2);
CHECK(!file_change.requires_refinement);
}
#endif
}
};
F().run();
}
void test_hierarchies() {
struct F : fixture_real_t {
using fixture_real_t::fixture_real_t;
void main() noexcept override {
using names_t = std::vector<std::wstring_view>;
auto folder_id = std::string("my-folder-id");
auto back_addr = sup->get_address();
SECTION("remove hierarchy") {
if (!test::wine_environment()) {
auto path_1 = root_path / L"x" / L"y" / L"файл.bin";
auto path_2 = root_path / L"a" / L"b";
bfs::create_directories(path_1.parent_path());
bfs::create_directories(path_2);
write_file(path_1, "12345");
watch_folder(folder_id);
auto names = names_t({L"a", L"a/b", L"x", L"x/y", L"x/y/файл.bin"});
SECTION("remove files individually") {
for (auto it = names.rbegin(); it != names.rend(); ++it) {
auto p = root_path / bfs::path(*it);
bfs::remove_all(p);
}
}
SECTION("remove subdirs ") {
auto for_removal = names_t({L"a", L"x"});
for (auto it = for_removal.rbegin(); it != for_removal.rend(); ++it) {
auto p = root_path / bfs::path(*it);
bfs::remove_all(p);
}
}
await_events(poll_t::trigger_timer, 5, true);
for (auto i = size_t{0}; i < names.size(); ++i) {
auto &payload = changes[i]->payload;
REQUIRE(payload.size() == 1);
auto &file_change = payload[0].file_changes.front();
auto &name = names[i];
CHECK(proto::get_name(file_change) == narrow(names[i]));
CHECK(file_change.update_reason == update_type_t::deleted);
}
#ifndef SYNCSPIRIT_WIN
auto impl = static_cast<watch_actor_t *>(target.get());
CHECK(impl->path_map.size() == 1);
CHECK(impl->subdir_map.size() == 1);
#endif
}
}
SECTION("create hierarchy") {
if (!test::wine_environment()) {
watch_folder(folder_id);
auto names = names_t({L"a", L"a/b", L"x", L"x/y", L"x/y/z"});
for (auto it = names.rbegin(); it != names.rend(); ++it) {
auto p = root_path / bfs::path(*it);
bfs::create_directories(p);
}
#ifndef SYNCSPIRIT_WIN
await_events(poll_t::trigger_timer, 2, true);
auto name_1 = proto::get_name(changes[0]->payload[0].file_changes.front());
auto name_2 = proto::get_name(changes[1]->payload[0].file_changes.front());
CHECK(name_1 == narrow(L"a"));
CHECK(name_2 == narrow(L"x"));
recurse_scan(root_path);
#else
await_events(poll_t::trigger_timer, 5, true);
for (auto i = size_t{0}; i < names.size(); ++i) {
auto &payload = changes[i]->payload;
REQUIRE(payload.size() == 1);
auto &file_change = payload[0].file_changes.front();
auto &name = names[i];
LOG_DEBUG(log, "i = {}", i);
CHECK(proto::get_name(file_change) == narrow(names[i]));
CHECK(file_change.update_reason == update_type_t::created);
}
#endif
auto path_f = root_path / L"x/y/z" / L"f.bin";
write_file(path_f, "12345");
await_events(poll_t::trigger_timer, 1, true);
auto name_f = proto::get_name(changes[0]->payload[0].file_changes.front());
CHECK(name_f == narrow(L"x/y/z/f.bin"));
}
}
SECTION("move hierarchy (inside folder)") {
if (!test::wine_environment()) {
auto names = names_t({L"a", L"a/b", L"x", L"x/y", L"x/y/z", L"a/файл.bin"});
auto dir_1 = root_path / "win32-hack" / L"директория-1";
auto dir_2 = root_path / "win32-hack" / L"директория-2";
for (auto it = names.rbegin(); it != names.rend(); ++it) {
auto p = dir_1 / bfs::path(*it);
if (p.filename().generic_wstring() == L"файл.bin") {
write_file(p, "12345");
} else {
bfs::create_directories(p);
}
}
watch_folder(folder_id);
native::rename(dir_1, dir_2);
#ifndef SYNCSPIRIT_WIN
await_events(poll_t::trigger_timer, 1);
auto &file_change = changes[0]->payload[0].file_changes[0];
CHECK(proto::get_name(file_change) == narrow(L"win32-hack/директория-2"));
CHECK(proto::get_size(file_change) == 0);
CHECK(!proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::DIRECTORY);
CHECK(file_change.update_reason == update_type_t::meta);
CHECK(file_change.prev_path == narrow(L"win32-hack/директория-1"));
{
auto file_prev = root_path / "win32-hack" / L"директория-2" / "a" / L"файл.bin";
auto file_new = root_path / "win32-hack" / L"директория-2" / "a" / L"ф.bin";
native::rename(file_prev, file_new);
await_events(poll_t::trigger_timer, 1);
auto &file_change = changes[0]->payload[0].file_changes[0];
CHECK(proto::get_name(file_change) == narrow(L"win32-hack/директория-2/a/ф.bin"));
CHECK(proto::get_size(file_change) == 5);
CHECK(!proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::meta);
CHECK(file_change.prev_path == narrow(L"win32-hack/директория-2/a/файл.bin"));
}
#else
await_events(poll_t::trigger_timer, 2, true);
{
auto &file_change = changes[0]->payload[0].file_changes[0];
CHECK(proto::get_name(file_change) == narrow(L"win32-hack/директория-1"));
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_deleted(file_change));
CHECK(file_change.update_reason == update_type_t::deleted);
CHECK(file_change.prev_path == "");
}
{
auto &file_change = changes[1]->payload[0].file_changes[0];
CHECK(proto::get_name(file_change) == narrow(L"win32-hack/директория-2"));
CHECK(proto::get_size(file_change) == 0);
CHECK(!proto::get_deleted(file_change));
CHECK(proto::get_type(file_change) == proto::FileInfoType::DIRECTORY);
CHECK(file_change.update_reason == update_type_t::created);
CHECK(file_change.prev_path == "");
CHECK(file_change.requires_refinement);
}
#endif
}
}
}
};
F().run();
}
void test_create_modify_rename() {
struct F : fixture_real_t {
using fixture_real_t::fixture_real_t;
void main() noexcept override {
auto folder_id = std::string("my-folder-id");
auto back_addr = sup->get_address();
watch_folder(folder_id);
#ifndef SYNCSPIRIT_WIN
auto path_file_tmp = root_path / L"файл.bin-tmp";
auto path_file_final = root_path / L"файл.bin";
write_file(path_file_tmp, "12345");
native::rename(path_file_tmp, path_file_final);
auto modified = fs::from_unix(123456);
bfs::last_write_time(path_file_final, modified);
await_events(poll_t::trigger_timer, 1);
#endif
};
};
F().run();
}
#if defined(SYNCSPIRIT_WATCHER_UNIX)
void test_unix_notification() {
struct F : fixture_real_t {
#ifdef SYNCSPIRIT_WATCHER_INOTIFY
#define IMPL_DIFF (-1)
#else
#define IMPL_DIFF 0
#endif
using fixture_real_t::fixture_real_t;
bool notify_upon_watch() override { return false; }
void main() noexcept override {
using child_info_t = fs::task::scan_dir_t::child_info_t;
using child_infos_t = fs::task::scan_dir_t::child_infos_t;
auto folder_id = std::string("my-folder-id");
auto back_addr = sup->get_address();
watch_folder(folder_id);
auto make_child = [](bfs::path path, bfs::file_type type = bfs::file_type::directory) -> child_info_t {
auto child = child_info_t{};
child.path = path;
child.status = bfs::file_status(type);
return child;
};
auto w = static_cast<fs::platform::unix::watcher_t *>(target.get());
CHECK(w->path_map.size() == 1);
CHECK(w->subdir_map.size() == 0);
SECTION("non-watched dir") {
auto task = fs::task::scan_dir_t(bfs::path("/some/path"), {}, {}, true, true, false);
task.child_infos = {make_child(bfs::path("/some/path/a"))};
w->notify(task);
CHECK(w->path_map.size() == 1);
CHECK(w->path_to_wd.size() == 1);
}
SECTION("dir with invalid name") {
auto invalid = std::uint8_t{201};
auto ptr = reinterpret_cast<char *>(&invalid);
auto invalid_name = std::string_view(ptr, ptr + 1);
auto task = fs::task::scan_dir_t(root_path, {}, {}, true, true, false);
task.child_infos = {make_child(bfs::path(invalid_name))};
w->notify(task);
CHECK(w->path_map.size() == 1);
CHECK(w->path_to_wd.size() == 1);
}
SECTION("non-watched dir of watched parent") {
auto task = fs::task::scan_dir_t(root_path, {}, {}, true, true, false);
auto child_path = root_path / "a";
bfs::create_directories(child_path);
task.child_infos = {make_child(child_path)};
w->notify(task);
CHECK(w->path_map.size() == 2);
CHECK(w->path_to_wd.size() == 2);
}
SECTION("non-watched dir of non-watched parent") {
auto task = fs::task::scan_dir_t(root_path / "xx", {}, {}, true, true, false);
auto dir_path = root_path / "xx";
bfs::create_directories(dir_path);
task.child_infos = {make_child(root_path / "xx/yy")};
w->notify(task);
CHECK(w->path_map.size() == 2);
CHECK(w->path_to_wd.size() == 2);
}
SECTION("watched dir") {
auto path_a = root_path / "a";
auto path_b = root_path / "b";
auto path_c = root_path / "c";
auto path_d = root_path / "d";
bfs::create_directories(path_a);
bfs::create_directories(path_c);
write_file(path_b, "");
bfs::create_symlink(bfs::path("/some/where"), path_d);
auto children = child_infos_t(
{make_child(path_a), make_child(path_b, bfs::file_type::regular), make_child(path_c)});
auto task = fs::task::scan_dir_t(root_path, {}, {}, true, true, false);
task.child_infos = children;
w->notify(task);
CHECK(w->path_map.size() == 4 + IMPL_DIFF);
CHECK(w->path_to_wd.size() == 4 + IMPL_DIFF);
// no-harm in double watch
w->notify(task);
CHECK(w->path_map.size() == 4 + IMPL_DIFF);
CHECK(w->path_to_wd.size() == 4 + IMPL_DIFF);
}
SECTION("watched sub-dir") {
auto sub_dir = root_path / L"папка";
bfs::create_directories(sub_dir);
await_events(poll_t::trigger_timer, 1);
auto path_a = sub_dir / "a";
auto path_b = sub_dir / "b";
auto path_c = sub_dir / "c";
bfs::create_directories(path_a);
bfs::create_directories(path_c);
write_file(path_b, "");
auto children = child_infos_t(
{make_child(path_a), make_child(path_b, bfs::file_type::regular), make_child(path_c)});
auto task = fs::task::scan_dir_t(sub_dir, {}, {}, true, true, false);
task.child_infos = children;
w->notify(task);
CHECK(w->path_map.size() == 5 + IMPL_DIFF);
CHECK(w->path_to_wd.size() == 5 + IMPL_DIFF);
// no-harm in double watch
w->notify(task);
CHECK(w->path_map.size() == 5 + IMPL_DIFF);
CHECK(w->path_to_wd.size() == 5 + IMPL_DIFF);
}
SECTION("tmp-file in a dir") {
auto path_tmp = root_path / "my-file.syncspirit-tmp";
write_file(path_tmp / "my-file.syncspirit-tmp", "12345");
auto children = child_infos_t({make_child(path_tmp, bfs::file_type::regular)});
auto task = fs::task::scan_dir_t(root_path, {}, {}, true, true, false);
task.child_infos = children;
w->notify(task);
CHECK(w->path_map.size() == 1);
CHECK(w->path_to_wd.size() == 1);
}
SECTION("error in watching") {
auto path_x = root_path / "a";
auto task = fs::task::scan_dir_t(root_path, {}, {}, true, true, false);
task.child_infos = {make_child(path_x)};
w->notify(task);
CHECK(w->path_map.size() == 1);
CHECK(w->path_to_wd.size() == 1);
}
}
};
F().run();
}
#endif
#ifdef SYNCSPIRIT_WATCHER_KQUEUE
void test_kqueue() {
struct F : fixture_real_t {
using fixture_real_t::fixture_real_t;
void main() noexcept override {
using child_info_t = fs::task::scan_dir_t::child_info_t;
using child_infos_t = fs::task::scan_dir_t::child_infos_t;
auto folder_id = std::string("my-folder-id");
auto back_addr = sup->get_address();
bfs::create_directories(root_path / "ex-dir");
bfs::create_directories(root_path / "ex-hier" / "aaa");
write_file(root_path / "ex-file", "12345");
bfs::create_symlink(root_path / "ex-target", root_path / "ex-link");
watch_folder(folder_id);
auto w = static_cast<fs::platform::unix::watcher_t *>(target.get());
SECTION("metadata (vnode/dir) events") {
auto watched_paths = w->path_map.size();
REQUIRE(watched_paths == 5);
auto expected_watches = watched_paths;
SECTION("creation") {
SECTION("new dir") { bfs::create_directories(root_path / "my-dir"); }
SECTION("new dir hierarchy") { bfs::create_directories(root_path / "a" / "b" / "c" / "d"); }
SECTION("new file") { write_file(root_path / "my-file", "12345"); }
SECTION("new link") { bfs::create_symlink(root_path / "a", root_path / "b"); }
}
SECTION("removal") {
SECTION("simple") {
auto name_raw = GENERATE("ex-file", "ex-dir");
auto name = std::string_view(name_raw);
bfs::remove(root_path / name);
--expected_watches;
}
SECTION("link") { bfs::remove(root_path / "ex-link"); }
SECTION("hierarchy") {
bfs::remove_all(root_path / "ex-hier");
expected_watches -= 2;
}
}
SECTION("renaming") {
SECTION("simple") {
auto name_raw = GENERATE("ex-file", "ex-dir");
auto name = std::string_view(name_raw);
spdlog::info("renaming {}", name);
bfs::rename(root_path / name, root_path / L"новое-имя");
--expected_watches;
}
SECTION("link") { bfs::rename(root_path / "ex-link", root_path / L"новое-имя"); }
SECTION("hierarchy") {
bfs::rename(root_path / "ex-hier", root_path / L"новое-имя");
expected_watches -= 2;
}
}
await_events(poll_t::trigger_timer, 1);
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == "");
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_type(file_change) == proto::FileInfoType::DIRECTORY);
CHECK(file_change.update_reason == update_type_t::content);
changes.clear();
}
await_events(poll_t::trigger_timer);
REQUIRE(changes.size() == 0);
CHECK(w->path_map.size() == expected_watches);
}
SECTION("file content change") {
write_file(root_path / "ex-file", "abcdef");
await_events(poll_t::trigger_timer, 1);
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == "ex-file");
CHECK(proto::get_size(file_change) == 6);
CHECK(proto::get_type(file_change) == proto::FileInfoType::FILE);
CHECK(file_change.update_reason == update_type_t::content);
changes.clear();
}
await_events(poll_t::trigger_timer);
REQUIRE(changes.size() == 0);
}
SECTION("permissions change") {
auto name_raw = GENERATE("ex-file", "ex-dir", "ex-hier");
auto name = std::string_view(name_raw);
auto path = root_path / name;
auto perms = 0777;
bfs::permissions(path, static_cast<bfs::perms>(perms));
await_events(poll_t::trigger_timer, 1);
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == name);
CHECK(proto::get_permissions(file_change) == perms);
CHECK(file_change.update_reason == update_type_t::meta);
changes.clear();
}
await_events(poll_t::trigger_timer);
REQUIRE(changes.size() == 0);
}
SECTION("changes agggeration (2 new files + new perms => dir content change)") {
auto perms = 0777;
write_file(root_path / "ex-dir" / L"ф1.bin", "abcde");
write_file(root_path / "ex-dir" / L"ф2.bin", "12345");
bfs::permissions(root_path / "ex-dir", static_cast<bfs::perms>(perms));
await_events(poll_t::trigger_timer, 1);
{
auto &payload = changes.front()->payload;
REQUIRE(payload.size() == 1);
auto &folder_change = payload[0];
REQUIRE(folder_change.folder_id == folder_id);
REQUIRE(folder_change.file_changes.size() == 1);
auto &file_change = folder_change.file_changes.front();
CHECK(proto::get_name(file_change) == "ex-dir");
CHECK(proto::get_permissions(file_change) == perms);
CHECK(proto::get_size(file_change) == 0);
CHECK(proto::get_type(file_change) == proto::FileInfoType::DIRECTORY);
CHECK(file_change.update_reason == update_type_t::content);
changes.clear();
}
await_events(poll_t::trigger_timer);
REQUIRE(changes.size() == 0);
}
}
};
F().run();
}
#endif
int _init() {
test::init_logging();
REGISTER_TEST_CASE(test_watcher_base, "test_watcher_base", "[fs]");
REGISTER_TEST_CASE(test_start_n_shutdown, "test_start_n_shutdown", "[fs]");
REGISTER_TEST_CASE(test_watch_unwatch, "test_watch_unwatch", "[fs]");
#ifdef SYNCSPIRIT_WATCHER_ANY
REGISTER_TEST_CASE(test_tmp_ignoring, "test_tmp_ignoring", "[fs]");
#endif
#ifndef SYNCSPIRIT_WATCHER_KQUEUE
REGISTER_TEST_CASE(test_real_impl, "test_real_impl", "[fs]");
REGISTER_TEST_CASE(test_hierarchies, "test_hierarchies", "[fs]");
REGISTER_TEST_CASE(test_create_modify_rename, "test_create_modify_rename", "[fs]");
#endif
#ifdef SYNCSPIRIT_WATCHER_UNIX
REGISTER_TEST_CASE(test_unix_notification, "test_unix_notification", "[fs]");
#endif
#ifdef SYNCSPIRIT_WATCHER_KQUEUE
REGISTER_TEST_CASE(test_kqueue, "test_kqueue", "[fs]");
#endif
return 1;
}
static int v = _init();
#endif
TEST_CASE("dummy-test", "test") { CHECK(true); }