Files
syncspirit/tests/089-local_keeper-changes.cpp

2568 lines
100 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 Ivan Baidakou
#include "access.h"
#include "diff-builder.h"
#include "config/fs.h"
#include "fs/fs_slave.h"
#include "fs/messages.h"
#include "fs/utils.h"
#include "model/cluster.h"
#include "model/diff/advance/advance.h"
#include "net/local_keeper.h"
#include "net/names.h"
#include "test-utils.h"
#include "test_supervisor.h"
#include "access.h"
#include "utils/platform.h"
#include "utils/utf8.h"
#include <chrono>
#include <variant>
#include <boost/nowide/convert.hpp>
#include <catch2/generators/catch_generators.hpp>
#ifndef SYNCSPIRIT_WIN
#include <sys/types.h>
#include <sys/stat.h>
#endif
using namespace syncspirit;
using namespace syncspirit::test;
using namespace syncspirit::model;
using namespace syncspirit::net;
using namespace syncspirit::fs;
using namespace syncspirit::hasher;
using boost::nowide::narrow;
struct fixture_t;
using I = syncspirit_watcher_impl_t;
using FT = proto::FileInfoType;
static constexpr auto default_perms = std::uint32_t{0123};
static constexpr auto default_perms_fs = static_cast<bfs::perms>(default_perms);
#ifndef SYNCSPIRIT_WIN
static constexpr auto expected_perms = std::uint32_t{0123};
#else
static constexpr auto expected_perms = std::uint32_t{0666};
#endif
struct my_supervisort_t : supervisor_t {
using parent_t = supervisor_t;
using parent_t::parent_t;
using strings_t = std::vector<std::string>;
outcome::result<void> operator()(const model::diff::local::file_availability_t &diff,
void *custom) noexcept override {
++file_availabilities;
return parent_t::operator()(diff, custom);
}
outcome::result<void> operator()(const model::diff::advance::advance_t &diff, void *custom) noexcept override {
auto name = proto::get_name(diff.proto_local);
updated_names.push_back(std::string(name));
return parent_t::operator()(diff, custom);
}
strings_t updated_names;
std::uint_fast32_t file_availabilities = 0;
fixture_t *fixture = nullptr;
};
struct fixture_t {
using target_ptr_t = r::intrusive_ptr_t<net::local_keeper_t>;
using builder_ptr_t = std::unique_ptr<diff_builder_t>;
using watch_folder_msg_t = r::intrusive_ptr_t<fs::message::watch_folder_t>;
using unwatch_folder_msg_t = r::intrusive_ptr_t<fs::message::unwatch_folder_t>;
using create_dir_msg_t = r::intrusive_ptr_t<fs::message::create_dir_t>;
fixture_t() noexcept { log = utils::get_logger("fixture"); }
virtual std::uint32_t get_hash_limit() { return 1; }
virtual void on_watch_folder(fs::message::watch_folder_t &msg) {
CHECK(!watch_folder_msg);
watch_folder_msg = &msg;
}
virtual void on_unwatch_folder(fs::message::unwatch_folder_t &msg) {
CHECK(!unwatch_folder_msg);
unwatch_folder_msg = &msg;
}
virtual void on_create_dir(fs::message::create_dir_t &msg) {
CHECK(!create_dir_msg);
create_dir_msg = &msg;
}
virtual void on_exec(fs::message::foreign_executor_t &msg) { LOG_WARN(log, "on_exec() is not implemented"); }
void run() noexcept {
sequencer = make_sequencer(1234);
auto my_hash = "KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD";
auto my_id = device_id_t::from_string(my_hash).value();
local_device = device_t::create(my_id, "my-device").value();
auto peer_hash = "VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ";
auto peer_id = device_id_t::from_string(peer_hash).value();
peer_device = device_t::create(peer_id, "peer_device").value();
cluster = new cluster_t(local_device, 1);
cluster->get_devices().put(local_device);
cluster->get_devices().put(peer_device);
r::system_context_t ctx;
sup = ctx.create_supervisor<my_supervisort_t>()
.make_presentation(true)
.timeout(timeout)
.create_registry()
.finish();
sup->cluster = cluster;
static_cast<my_supervisort_t *>(sup.get())->fixture = this;
sup->configure_callback = [&](r::plugin::plugin_base_t &plugin) {
plugin.template with_casted<r::plugin::registry_plugin_t>([&](auto &p) {
p.register_name(net::names::fs_actor, sup->get_address());
p.register_name(net::names::watcher, sup->get_address());
});
plugin.template with_casted<r::plugin::starter_plugin_t>([&](auto &p) {
using watch_msg_t = fs::message::watch_folder_t;
using unwatch_msg_t = fs::message::unwatch_folder_t;
using create_dir_msg_t = fs::message::create_dir_t;
using exec_msg_t = fs::message::foreign_executor_t;
p.subscribe_actor(r::lambda<watch_msg_t>([&](watch_msg_t &msg) { on_watch_folder(msg); }));
p.subscribe_actor(r::lambda<unwatch_msg_t>([&](unwatch_msg_t &msg) { on_unwatch_folder(msg); }));
p.subscribe_actor(r::lambda<create_dir_msg_t>([&](create_dir_msg_t &msg) { on_create_dir(msg); }));
p.subscribe_actor(r::lambda<exec_msg_t>([&](exec_msg_t &msg) { on_exec(msg); }));
});
};
sup->start();
sup->do_process();
builder = std::make_unique<diff_builder_t>(*cluster);
CHECK(static_cast<r::actor_base_t *>(sup.get())->access<to::state>() == r::state_t::OPERATIONAL);
sup->do_process();
auto fs_config = config::fs_config_t{3600, 10};
main();
sup->do_process();
sup->shutdown();
sup->do_process();
CHECK(static_cast<r::actor_base_t *>(sup.get())->access<to::state>() == r::state_t::SHUT_DOWN);
}
void launch_target(syncspirit_watcher_impl_t impl) {
target = sup->create_actor<net::local_keeper_t>()
.timeout(timeout)
.sequencer(sequencer)
.concurrent_hashes(get_hash_limit())
.watcher_impl(impl)
.finish();
sup->do_process();
sup->send<model::payload::thread_ready_t>(sup->get_address(), cluster, std::this_thread::get_id());
sup->send<model::payload::local_ready_t>(sup->get_address());
sup->do_process();
}
void submit(r::message_ptr_t message) noexcept {
message->address = std::move(message->next_route);
sup->put(message);
sup->do_process();
}
virtual void main() noexcept {}
std::int64_t files_scan_iteration_limit = 100;
builder_ptr_t builder;
r::pt::time_duration timeout = r::pt::millisec{10};
r::intrusive_ptr_t<my_supervisort_t> sup;
cluster_ptr_t cluster;
device_ptr_t local_device;
device_ptr_t peer_device;
utils::logger_t log;
target_ptr_t target;
model::sequencer_ptr_t sequencer;
watch_folder_msg_t watch_folder_msg;
unwatch_folder_msg_t unwatch_folder_msg;
create_dir_msg_t create_dir_msg;
};
void test_just_start() {
struct F : fixture_t {
using fixture_t::fixture_t;
void main() noexcept override {
auto impl = GENERATE(I::none, I::inotify, I::kqueue, I::win32);
launch_target(impl);
CHECK(static_cast<r::actor_base_t *>(target.get())->access<to::state>() == r::state_t::OPERATIONAL);
sup->do_process();
}
};
F().run();
}
void test_watch_unwatch() {
struct F : fixture_t {
using fixture_t::fixture_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
auto folder_id = "1234-5678";
db::Folder db_folder;
db::set_id(db_folder, folder_id);
db::set_label(db_folder, folder_id);
db::set_path(db_folder, "/some/path");
db::set_folder_type(db_folder, db::FolderType::send_and_receive);
SECTION("folder is created before start => watched upon app start") {
db::set_watched(db_folder, true);
builder->upsert_folder(db_folder, 5).apply(*sup);
auto folder = cluster->get_folders().by_id(folder_id);
REQUIRE(!create_dir_msg);
launch_target(impl);
REQUIRE(!create_dir_msg);
REQUIRE(watch_folder_msg);
REQUIRE(!unwatch_folder_msg);
auto &p = watch_folder_msg->payload;
CHECK(p.folder_id == folder_id);
CHECK(p.path == "/some/path");
p.ec = {};
submit(std::move(watch_folder_msg));
SECTION("another upsert") {
builder->upsert_folder(db_folder, 5).apply(*sup);
REQUIRE(!watch_folder_msg);
REQUIRE(!unwatch_folder_msg);
}
SECTION("update folder (non-wached) => send unwatch") {
db::set_watched(db_folder, false);
builder->upsert_folder(db_folder, 5).apply(*sup);
CHECK(!watch_folder_msg);
REQUIRE(unwatch_folder_msg);
auto &p = unwatch_folder_msg->payload;
CHECK(p.folder_id == folder_id);
}
SECTION("remove folder => send unwatch") {
builder->remove_folder(*folder).apply(*sup);
REQUIRE(unwatch_folder_msg);
auto &p = unwatch_folder_msg->payload;
CHECK(p.folder_id == folder_id);
}
}
SECTION("post-start create folder & watch") {
launch_target(impl);
CHECK(static_cast<r::actor_base_t *>(target.get())->access<to::state>() == r::state_t::OPERATIONAL);
sup->do_process();
SECTION("create non-watched folder") {
db::set_watched(db_folder, false);
builder->upsert_folder(db_folder, 5).apply(*sup);
CHECK(create_dir_msg);
REQUIRE(!watch_folder_msg);
}
SECTION("create watched folder") {
db::set_watched(db_folder, true);
builder->upsert_folder(db_folder, 5).apply(*sup);
REQUIRE(create_dir_msg);
create_dir_msg->payload.ec = {};
submit(std::move(create_dir_msg));
REQUIRE(watch_folder_msg);
auto &p = watch_folder_msg->payload;
CHECK(p.folder_id == folder_id);
CHECK(p.path == "/some/path");
}
}
}
};
F().run();
}
struct folder_fixture_t : fixture_t {
using parent_t = fixture_t;
using parent_t::parent_t;
using hashed_blocks_t = std::list<utils::bytes_t>;
using child_infos_t = fs::task::scan_dir_t::child_infos_t;
using dir_result_t = std::variant<fs::task::scan_dir_t::child_infos_t, sys::error_code>;
using dir_children_t = std::list<dir_result_t>;
using child_info_t = fs::task::scan_dir_t::child_info_t;
void on_watch_folder(fs::message::watch_folder_t &msg) override {
auto &p = msg.payload;
p.ec = {};
LOG_DEBUG(log, "watching {}", p.folder_id);
watched_ack = true;
}
void on_unwatch_folder(fs::message::unwatch_folder_t &msg) override {
watched_ack = false;
auto &p = msg.payload;
p.ec = {};
LOG_DEBUG(log, "unwatching {}", p.folder_id);
}
void on_create_dir(fs::message::create_dir_t &msg) override {
auto &p = msg.payload;
p.ec = {};
LOG_DEBUG(log, "creating a dir for {}", p.folder_id, narrow(p.generic_wstring()));
}
static child_info_t make_child(std::string_view name, bfs::file_type type = bfs::file_type::directory,
std::uintmax_t size = 0, std::uint32_t perms = default_perms,
std::int64_t modified = 0) {
auto child = child_info_t{};
child.path = bfs::path(name);
child.status = bfs::file_status(type, static_cast<bfs::perms>(perms));
child.size = size;
child.last_write_time = fs::from_unix(modified);
return child;
};
virtual bool process_cmd(fs::task::scan_dir_t &task) noexcept {
auto path = narrow(task.path.generic_wstring());
if (!dir_children.empty()) {
std::visit(
[&](auto &item) {
using T = std::decay_t<decltype(item)>;
if constexpr (std::is_same_v<T, sys::error_code>) {
task.ec = item;
LOG_DEBUG(log, "process_cmd(scan_dir_t) '{}' -> error: {}", path, item.message());
task.child_infos.clear();
} else {
task.ec = {};
LOG_DEBUG(log, "process_cmd(scan_dir_t) '{}' -> {} items", path, item.size());
task.child_infos = std::move(item);
}
},
dir_children.front());
dir_children.pop_front();
return true;
} else {
LOG_DEBUG(log, "process_cmd(scan_dir_t) '{}' -> no results", path);
}
return false;
}
virtual bool process_cmd(fs::task::segment_iterator_t &task) noexcept {
static const constexpr size_t SZ = SHA256_DIGEST_LENGTH;
LOG_DEBUG(log, "process_cmd(segment_iterator_t) {}", narrow(task.path.generic_wstring()));
for (std::int32_t i = task.block_index, j = 0; j < task.block_count; ++i, ++j) {
auto bs = (j + 1 == task.block_count) ? task.last_block_size : task.block_size;
auto off = task.offset + std::int64_t{task.block_size} * j;
REQUIRE(hashed_blocks.size());
auto &data = hashed_blocks.front();
auto tmp_addr = sup->get_registry_address();
auto dst_addr = target->get_address();
auto digest = r::make_routed_message<hasher::payload::digest_t>(tmp_addr, dst_addr, data, i, task.context);
auto digetst_backend = static_cast<hasher::message::digest_t *>(digest.get());
unsigned char d[SZ];
utils::digest(data.data(), data.size(), d);
digetst_backend->payload.result = utils::bytes_t(d, d + SZ);
hashed_blocks.pop_front();
sup->put(std::move(digest));
return true;
}
return false;
}
virtual bool process_cmd(fs::task::remove_file_t &task) noexcept {
LOG_DEBUG(log, "process_cmd(remove_file_t) {}", narrow(task.path.generic_wstring()));
return false;
}
virtual bool process_cmd(fs::task::rename_file_t &task) noexcept {
LOG_DEBUG(log, "process_cmd(rename_file_t) {}", narrow(task.path.generic_wstring()));
return false;
}
virtual bool process_cmd(fs::task::noop_t &task) noexcept {
LOG_DEBUG(log, "process_cmd(noop_t");
return false;
}
void on_exec(fs::message::foreign_executor_t &msg) override {
LOG_INFO(log, "on_exec()");
auto slave = dynamic_cast<fs::fs_slave_t *>(msg.payload.get());
for (auto &t : slave->tasks_in) {
auto processed = std::visit([&](auto &task) -> bool { return process_cmd(task); }, t);
if (processed) {
slave->ec = {};
}
}
slave->tasks_out = std::move(slave->tasks_in);
}
void prepare(syncspirit_watcher_impl_t impl, bool watch_folder = true) noexcept {
db::Folder db_folder;
db::set_id(db_folder, folder_id);
db::set_label(db_folder, folder_id);
db::set_path(db_folder, "/some/path");
db::set_watched(db_folder, watch_folder);
builder->upsert_folder(db_folder, 5).apply(*sup);
folder = cluster->get_folders().by_id(folder_id);
folder_local = folder->get_folder_infos().by_device(*local_device);
files_local = &folder_local->get_file_infos();
launch_target(impl);
REQUIRE(watched_ack);
}
void expect_bytes_hash(utils::bytes_view_t bytes) noexcept { hashed_blocks.emplace_back(utils::bytes_t(bytes)); }
void expect_dir_scan(child_infos_t children, bool back = true) noexcept {
if (back) {
dir_children.emplace_back(std::move(children));
} else {
dir_children.emplace_front(std::move(children));
}
}
void expect_dir_scan_error(sys::error_code ec) noexcept { dir_children.emplace_back(std::move(ec)); }
void mk_update(proto::FileInfo info, fs::update_type_t update_type, bool refine, bool process = true) noexcept {
auto change = fs::payload::file_info_t(std::move(info), {}, update_type, refine);
auto changes = fs::payload::file_changes_t{{std::move(change)}};
auto folder_change = fs::payload::folder_change_t{folder_id, std::move(changes)};
auto folder_changes = fs::payload::folder_changes_t{{std::move(folder_change)}};
auto &addr = sup->get_address();
LOG_DEBUG(log, "making update...");
sup->send<fs::payload::folder_changes_t>(addr, std::move(folder_changes));
if (process) {
sup->do_process();
}
}
void make_update_rename(proto::FileInfo info, std::string_view prev_name, bool process = true) noexcept {
auto change = fs::payload::file_info_t(std::move(info), std::string(prev_name), fs::update_type_t::meta, false);
auto changes = fs::payload::file_changes_t{{std::move(change)}};
auto folder_change = fs::payload::folder_change_t{folder_id, std::move(changes)};
auto folder_changes = fs::payload::folder_changes_t{{std::move(folder_change)}};
auto &addr = sup->get_address();
sup->send<fs::payload::folder_changes_t>(addr, std::move(folder_changes));
if (process) {
sup->do_process();
}
}
std::string folder_id = "1234-5678";
bool watched_ack = false;
model::folder_ptr_t folder;
model::folder_info_ptr_t folder_local;
model::file_infos_map_t *files_local = nullptr;
hashed_blocks_t hashed_blocks;
dir_children_t dir_children;
};
void test_trivial_changes() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
LOG_INFO(log, "impl: {}", static_cast<int>(impl));
auto file = proto::FileInfo();
auto file_name = std::string_view("some-file-name.bin");
proto::set_name(file, file_name);
proto::set_permissions(file, default_perms);
proto::set_modified_s(file, 12345);
SECTION("create new dir/symlink/emty-file") {
auto file_type = GENERATE(FT::DIRECTORY, FT::FILE, FT::SYMLINK);
proto::set_type(file, file_type);
if (file_type == FT::SYMLINK) {
proto::set_symlink_target(file, "/some/target");
}
if (file_type == FT::DIRECTORY) {
expect_dir_scan({});
}
mk_update(file, fs::update_type_t::created, file_type == FT::DIRECTORY);
CHECK(files_local->size() == 1);
auto f = files_local->by_name(file_name);
REQUIRE(f);
#ifndef SYNCSPIRIT_WIN
CHECK(f->get_permissions() == expected_perms);
#endif
CHECK(f->get_modified_s() == 12345);
CHECK(f->get_size() == 0);
if (file_type == FT::DIRECTORY) {
CHECK(f->is_dir());
}
if (file_type == FT::FILE) {
CHECK(f->is_file());
}
if (file_type == FT::SYMLINK) {
CHECK(f->is_link());
CHECK(f->get_link_target() == "/some/target");
}
}
SECTION("updates on existing") {
proto::set_type(file, FT::FILE);
builder->local_update(folder_id, file).apply(*sup);
auto f = files_local->by_name(file_name);
CHECK(f->get_permissions() == 0123);
REQUIRE(f);
auto file_seq = f->get_sequence();
auto folder_seq = folder_local->get_max_sequence();
auto update_type = fs::update_type_t::meta;
SECTION("update metadata") {
proto::set_permissions(file, 0777);
update_type = fs::update_type_t::meta;
mk_update(file, update_type, false);
CHECK(f->get_permissions() == 0777);
CHECK(!f->is_deleted());
}
SECTION("delete file") {
proto::set_deleted(file, true);
update_type = fs::update_type_t::deleted;
mk_update(file, update_type, false);
CHECK(f->get_permissions() == 0123);
CHECK(f->is_deleted());
}
auto file_seq_2 = f->get_sequence();
auto folder_seq_2 = folder_local->get_max_sequence();
CHECK(file_seq_2 > file_seq);
CHECK(folder_seq_2 > folder_seq);
mk_update(file, update_type, false);
CHECK(f->get_sequence() == file_seq_2);
CHECK(folder_local->get_max_sequence() == folder_seq_2);
}
SECTION("create & remove a dir") {
proto::set_type(file, FT::DIRECTORY);
expect_dir_scan({});
mk_update(file, fs::update_type_t::created, true);
CHECK(files_local->size() == 1);
auto f = files_local->by_name(file_name);
REQUIRE(f);
CHECK(f->is_dir());
auto seq = f->get_sequence();
proto::set_type(file, FT::FILE); // it not known type of already removed file
proto::set_deleted(file, true);
mk_update(file, fs::update_type_t::deleted, false);
CHECK(f->get_sequence() > seq);
CHECK(f->is_deleted());
CHECK(f->is_dir());
}
SECTION("ignore parent-less dir creation") {
proto::set_name(file, "dir/sub-dir");
proto::set_type(file, FT::DIRECTORY);
mk_update(file, fs::update_type_t::created, false);
CHECK(files_local->size() == 0);
}
SECTION("ignore event with tmp-filename") {
proto::set_name(file, "my.syncspirit-tmp");
proto::set_type(file, FT::FILE);
proto::set_size(file, 5);
mk_update(file, fs::update_type_t::created, false);
CHECK(files_local->size() == 0);
}
}
};
F().run();
}
void test_ignoring_tmps() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
bool process_cmd(fs::task::remove_file_t &task) noexcept override {
++file_removal_attempts;
return parent_t::process_cmd(task);
return false;
}
void main() noexcept override {
using namespace std::chrono;
using UT = fs::update_type_t;
using Clock = steady_clock;
auto now = std::chrono::system_clock::now();
auto seconds = std::chrono::system_clock::to_time_t(now);
SECTION("direct update of tmp") {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
auto file_name = fmt::format("some-file-name.bin{}", fs::tmp_suffix);
auto file = proto::FileInfo();
proto::set_name(file, file_name);
proto::set_permissions(file, default_perms);
proto::set_type(file, FT::FILE);
proto::set_modified_s(file, seconds);
auto update_type = GENERATE(UT::created, UT::content, UT::deleted, UT::meta);
mk_update(file, update_type, false);
CHECK(files_local->size() == 0);
}
SECTION("indirect update of tmp") {
auto impl = GENERATE(I::kqueue);
prepare(impl);
auto file = proto::FileInfo();
proto::set_name(file, "dir");
proto::set_permissions(file, default_perms);
proto::set_type(file, FT::DIRECTORY);
expect_dir_scan({});
mk_update(file, UT::content, true);
auto f = files_local->begin()->get();
auto sequence = f->get_sequence();
auto path = fmt::format("/some/path/dir/some-file-name.bin{}", fs::tmp_suffix);
CHECK(files_local->size() == 1);
auto dir_children = child_infos_t{make_child(path, bfs::file_type::regular, 5, default_perms, seconds)};
expect_dir_scan(dir_children);
mk_update(file, UT::content, true);
CHECK(files_local->size() == 1);
CHECK(f->get_sequence() == sequence);
CHECK(file_removal_attempts == 0);
}
SECTION("ignore locked file") {
auto impl = GENERATE(I::kqueue);
prepare(impl);
auto file = proto::FileInfo();
proto::set_name(file, "dir");
proto::set_permissions(file, default_perms);
proto::set_type(file, FT::DIRECTORY);
auto path = fmt::format("/some/path/dir/some-file-name.bin");
{
auto dir_children = child_infos_t{make_child(path, bfs::file_type::regular, 0, default_perms)};
expect_dir_scan(dir_children);
}
mk_update(file, UT::content, true);
proto::set_name(file, "dir/some-file-name.bin");
proto::set_type(file, FT::FILE);
builder->local_update(folder_id, file);
REQUIRE(files_local->size() == 2);
auto sha256 = peer_device->device_id().get_sha256();
builder->share_folder(peer_device->device_id().get_sha256(), folder_id).apply(*sup);
auto folder_peer = folder_local->get_folder()->get_folder_infos().by_device(*peer_device);
REQUIRE(folder_peer);
auto f = files_local->by_name("dir/some-file-name.bin");
auto version = f->get_version().as_proto();
auto counter = proto::Counter();
proto::set_id(counter, peer_device->device_id().get_uint());
proto::set_value(counter, 100);
proto::add_counters(version, counter);
proto::set_version(file, version);
proto::set_sequence(file, 10);
proto::set_size(file, 10);
auto &block = proto::add_blocks(file);
proto::set_hash(block, as_bytes("abc"));
proto::set_size(block, 10);
builder->configure_cluster(sha256)
.add(sha256, folder_id, folder_peer->get_index(), 10)
.finish()
.apply(*sup);
builder->make_index(sha256, folder_id).add(file, peer_device, false).finish().apply(*sup);
auto peer_file = folder_peer->get_file_infos().begin()->get();
REQUIRE(peer_file);
auto file_guard = peer_file->guard(*folder_peer);
auto path_tmp = fmt::format("/some/path/dir/some-file-name.bin{}", fs::tmp_suffix);
auto dir_children = child_infos_t{make_child(path_tmp, bfs::file_type::regular, 10, default_perms),
make_child(path, bfs::file_type::regular, 0, default_perms)};
expect_dir_scan(dir_children);
auto sequence = f->get_sequence();
proto::set_name(file, "dir");
proto::set_type(file, FT::DIRECTORY);
proto::set_size(file, 0);
mk_update(file, UT::content, true);
CHECK(files_local->size() == 2);
CHECK(f->get_sequence() == sequence);
CHECK(file_removal_attempts == 0);
}
}
int file_removal_attempts = 0;
};
F().run();
}
void test_hashing() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
auto pr_dir_1 = proto::FileInfo();
proto::set_name(pr_dir_1, narrow(L"папка1"));
proto::set_type(pr_dir_1, FT::DIRECTORY);
builder->local_update(folder_id, pr_dir_1).apply(*sup);
auto pr_dir_2 = proto::FileInfo();
proto::set_name(pr_dir_2, narrow(L"папка1/подпапка2"));
proto::set_type(pr_dir_2, FT::DIRECTORY);
builder->local_update(folder_id, pr_dir_2).apply(*sup);
auto pr_file = proto::FileInfo();
auto file_namew = GENERATE(L"файл.bin", L"папка1/файл.bin", L"папка1/подпапка2/файл.bin");
auto file_name = narrow(file_namew);
proto::set_name(pr_file, file_name);
proto::set_permissions(pr_file, default_perms);
proto::set_modified_s(pr_file, 12345);
proto::set_type(pr_file, FT::FILE);
proto::set_size(pr_file, 5);
expect_bytes_hash(as_bytes("12345"));
SECTION("new file created") { mk_update(pr_file, fs::update_type_t::created, false); }
SECTION("existing file content updated") {
proto::set_size(pr_file, 4);
builder->local_update(folder_id, pr_file).apply(*sup);
mk_update(pr_file, fs::update_type_t::content, false);
}
auto f = files_local->by_name(file_name);
REQUIRE(f);
CHECK(f->get_permissions() == expected_perms);
CHECK(f->get_modified_s() == 12345);
CHECK(f->get_size() == 5);
CHECK(f->is_file());
}
};
F().run();
}
void test_rescan() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
bool process_cmd(fs::task::scan_dir_t &task) noexcept override {
++dir_scans;
return parent_t::process_cmd(task);
}
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
auto pr_dir = proto::FileInfo();
proto::set_permissions(pr_dir, default_perms);
proto::set_type(pr_dir, FT::DIRECTORY);
for (auto &p : {"dir-a", "dir-b", "dir-a/x"}) {
proto::set_name(pr_dir, p);
builder->local_update(folder_id, pr_dir);
}
builder->apply(*sup);
CHECK(sup->file_availabilities == 0);
auto sequence = folder_local->get_max_sequence();
CHECK(sequence == 3);
auto root_children = child_infos_t{make_child("/some/path/dir-a"), make_child("/some/path/dir-b")};
auto a_children = child_infos_t{make_child("/some/path/dir-a/x")};
auto no_children = child_infos_t{};
SECTION("rescan whole dir => get updates") {
expect_dir_scan(root_children);
expect_dir_scan(no_children);
expect_dir_scan(a_children);
expect_dir_scan(no_children);
builder->scan_start(folder_id).apply(*sup);
CHECK(sup->file_availabilities == 3);
CHECK(folder_local->get_max_sequence() == sequence);
CHECK(files_local->size() == 3);
CHECK(dir_scans == 4);
}
SECTION("content update with recurse on") {
expect_dir_scan(root_children);
expect_dir_scan(no_children);
expect_dir_scan(a_children);
expect_dir_scan(no_children);
proto::set_name(pr_dir, "");
mk_update(pr_dir, fs::update_type_t::content, true, true);
CHECK(sup->file_availabilities == 0);
CHECK(folder_local->get_max_sequence() == sequence);
CHECK(files_local->size() == 3);
CHECK(dir_scans == 1);
}
}
int dir_scans = 0;
};
F().run();
}
void test_skip_scan_known() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
bool process_cmd(fs::task::scan_dir_t &task) noexcept override {
++dir_scans;
return parent_t::process_cmd(task);
}
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
auto pr_dir = proto::FileInfo();
proto::set_name(pr_dir, "dir-a");
proto::set_permissions(pr_dir, default_perms);
proto::set_modified_s(pr_dir, 12345);
proto::set_type(pr_dir, FT::DIRECTORY);
for (auto &p : {"dir-a", "dir-a/A", "dir-a/A/1", "dir-a/C"}) {
proto::set_name(pr_dir, p);
builder->local_update(folder_id, pr_dir).apply(*sup);
}
expect_dir_scan(
{make_child("/some/path/dir-a/A"), make_child("/some/path/dir-a/B"), make_child("/some/path/dir-a/C")});
expect_dir_scan({make_child("/some/path/dir-a/B/2")});
expect_dir_scan({});
proto::set_name(pr_dir, "dir-a");
proto::set_modified_s(pr_dir, 12346);
mk_update(pr_dir, fs::update_type_t::content, false);
CHECK(files_local->size() == 6);
auto dir_b = files_local->by_name("dir-a/B");
REQUIRE(dir_b);
CHECK(dir_b->is_dir());
CHECK(dir_b->is_locally_available());
auto dir_b2 = files_local->by_name("dir-a/B/2");
REQUIRE(dir_b2);
CHECK(dir_b2->is_dir());
CHECK(dir_b2->is_locally_available());
auto dir_c = files_local->by_name("dir-a/C");
REQUIRE(dir_c);
CHECK(dir_c->is_dir());
CHECK(dir_c->is_locally_available());
auto dir_c_seq = dir_c->get_sequence();
CHECK(dir_scans == 3);
expect_dir_scan({make_child("/some/path/dir-a/A"), make_child("/some/path/dir-a/B")});
mk_update(pr_dir, fs::update_type_t::content, false);
auto dir_c_new = files_local->by_name("dir-a/C");
REQUIRE(dir_c_new);
CHECK(dir_c_new->is_dir());
CHECK(dir_c_new->is_deleted());
CHECK(dir_c_new->get_sequence() > dir_c_seq);
CHECK(dir_scans == 4);
}
int dir_scans = 0;
};
F().run();
}
void test_new_dir_refinement() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
bool process_cmd(fs::task::scan_dir_t &task) noexcept override {
if (task.notify) {
++watcher_notifications;
}
return parent_t::process_cmd(task);
}
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
auto data = as_owned_bytes("12345");
auto data_h = utils::sha256_digest(data).value();
auto pr_dir = proto::FileInfo();
proto::set_name(pr_dir, "dir-a");
proto::set_permissions(pr_dir, default_perms);
proto::set_modified_s(pr_dir, 12345);
proto::set_type(pr_dir, FT::DIRECTORY);
expect_dir_scan({make_child("/some/path/dir-a/dir-b")});
expect_dir_scan({make_child("/some/path/dir-a/dir-b/dir-c"),
make_child("/some/path/dir-a/dir-b/file.bin", bfs::file_type::regular, 5)});
expect_dir_scan({});
expect_bytes_hash(as_bytes("12345"));
mk_update(pr_dir, fs::update_type_t::created, true);
auto dir_a = files_local->by_name("dir-a");
REQUIRE(dir_a);
CHECK(dir_a->is_dir());
CHECK(dir_a->is_locally_available());
auto dir_b = files_local->by_name("dir-a/dir-b");
REQUIRE(dir_b);
CHECK(dir_b->is_dir());
CHECK(dir_b->is_locally_available());
auto dir_c = files_local->by_name("dir-a/dir-b/dir-c");
REQUIRE(dir_c);
CHECK(dir_c->is_dir());
CHECK(dir_c->is_locally_available());
auto file = files_local->by_name("dir-a/dir-b/file.bin");
REQUIRE(file);
CHECK(file->is_file());
CHECK(file->is_locally_available());
CHECK(file->get_size() == 5);
REQUIRE(file->get_block_size() == 5);
auto b = file->iterate_blocks().next();
REQUIRE(b);
CHECK(b->get_hash() == data_h);
CHECK(watcher_notifications == 3);
}
int watcher_notifications = 0;
};
F().run();
}
void test_new_dir_without_refinement() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
auto data = as_owned_bytes("12345");
auto data_h = utils::sha256_digest(data).value();
auto pr_dir = proto::FileInfo();
proto::set_name(pr_dir, "dir-a");
proto::set_permissions(pr_dir, default_perms);
proto::set_modified_s(pr_dir, 12345);
proto::set_type(pr_dir, FT::DIRECTORY);
mk_update(pr_dir, fs::update_type_t::created, false);
CHECK(files_local->size() == 1);
auto dir_a = files_local->by_name("dir-a");
REQUIRE(dir_a);
CHECK(dir_a->is_dir());
CHECK(dir_a->is_locally_available());
}
};
F().run();
}
void test_remove_dir_refinement() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
expect_dir_scan({});
auto pr_dir = proto::FileInfo();
proto::set_permissions(pr_dir, default_perms);
proto::set_modified_s(pr_dir, 12345);
proto::set_type(pr_dir, FT::DIRECTORY);
auto names = {"dir", "dir/subdir_1", "dir/subdir_2"};
for (auto &name : names) {
proto::set_name(pr_dir, name);
builder->local_update(folder_id, pr_dir).apply(*sup);
}
REQUIRE(files_local->size() == 3);
auto seq = folder_local->get_max_sequence();
proto::set_name(pr_dir, "dir");
mk_update(pr_dir, fs::update_type_t::deleted, true, true);
CHECK(folder_local->get_max_sequence() == seq + 3);
auto dir = files_local->by_name("dir");
REQUIRE(dir);
CHECK(dir->is_dir());
CHECK(dir->is_deleted());
auto sub_dir_1 = files_local->by_name("dir/subdir_1");
REQUIRE(sub_dir_1);
CHECK(sub_dir_1->is_dir());
CHECK(sub_dir_1->is_deleted());
auto sub_dir_2 = files_local->by_name("dir/subdir_2");
REQUIRE(sub_dir_2);
CHECK(sub_dir_2->is_dir());
CHECK(sub_dir_2->is_deleted());
}
};
F().run();
}
void test_scan_notification_unix() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
bool process_cmd(fs::task::scan_dir_t &task) noexcept override {
if (task.notify) {
++watcher_notifications;
}
return parent_t::process_cmd(task);
}
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
expect_dir_scan({make_child("/some/path/dir-a")});
expect_dir_scan({make_child("/some/path/dir-a/dir-b")});
expect_dir_scan({});
LOG_INFO(log, "triggering scan...");
builder->scan_start(folder_id).apply(*sup);
CHECK(watcher_notifications == 3);
}
int watcher_notifications = 0;
};
F().run();
}
void test_kqueue_changes() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
void main() noexcept override {
auto impl = I::kqueue;
prepare(impl);
SECTION("root dir content") {
auto pr_dir = proto::FileInfo();
proto::set_name(pr_dir, "");
proto::set_type(pr_dir, FT::DIRECTORY);
expect_dir_scan({make_child("/some/path/dir-a")});
expect_dir_scan({make_child("/some/path/dir-a/dir-b")});
expect_dir_scan({});
mk_update(pr_dir, fs::update_type_t::content, false);
auto dir_a = files_local->by_name("dir-a");
REQUIRE(dir_a);
CHECK(dir_a->is_dir());
CHECK(dir_a->is_locally_available());
auto dir_b = files_local->by_name("dir-a/dir-b");
REQUIRE(dir_b);
CHECK(dir_b->is_dir());
CHECK(dir_b->is_locally_available());
}
}
};
F().run();
}
void test_dir_scan_errors() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
expect_dir_scan(
{make_child("/some/path/dir-a/A"), make_child("/some/path/dir-a/B"), make_child("/some/path/dir-a/C")});
expect_dir_scan({});
expect_dir_scan_error(utils::make_error_code(utils::error_code_t::no_action));
expect_dir_scan({});
auto pr_dir = proto::FileInfo();
proto::set_name(pr_dir, "dir-a");
proto::set_permissions(pr_dir, default_perms);
proto::set_modified_s(pr_dir, 12345);
proto::set_type(pr_dir, FT::DIRECTORY);
SECTION("new dir/update") {
mk_update(pr_dir, fs::update_type_t::created, true);
auto dir_a = files_local->by_name("dir-a");
REQUIRE(dir_a);
CHECK(dir_a->is_dir());
CHECK(dir_a->is_locally_available());
auto dir_A = files_local->by_name("dir-a/A");
REQUIRE(dir_A);
CHECK(dir_A->is_dir());
CHECK(dir_A->is_locally_available());
auto dir_B = files_local->by_name("dir-a/B");
CHECK(!dir_B);
auto dir_C = files_local->by_name("dir-a/C");
REQUIRE(dir_C);
CHECK(dir_C->is_dir());
CHECK(dir_C->is_locally_available());
}
SECTION("existing dirs") {
for (auto &p : {"dir-a", "dir-a/A", "dir-a/B", "dir-a/C"}) {
proto::set_name(pr_dir, p);
builder->local_update(folder_id, pr_dir).apply(*sup);
}
auto data = as_bytes("12345");
auto data_h = utils::sha256_digest(as_bytes("12345")).value();
auto b = proto::BlockInfo();
proto::set_hash(b, data_h);
proto::set_offset(b, 0);
proto::set_size(b, 5);
proto::set_name(pr_dir, "dir-a/B/x.bin");
proto::set_type(pr_dir, proto::FileInfoType::FILE);
proto::set_size(pr_dir, 5);
proto::add_blocks(pr_dir, b);
expect_bytes_hash(data);
builder->local_update(folder_id, pr_dir).apply(*sup);
expect_dir_scan({make_child("/some/path/dir-a")}, false);
LOG_INFO(log, "triggering scan...");
builder->scan_start(folder_id).apply(*sup);
auto dir_a = files_local->by_name("dir-a");
REQUIRE(dir_a);
CHECK(dir_a->is_dir());
CHECK(dir_a->is_locally_available());
auto dir_A = files_local->by_name("dir-a/A");
REQUIRE(dir_A);
CHECK(dir_A->is_dir());
CHECK(dir_A->is_locally_available());
auto dir_B = files_local->by_name("dir-a/B");
REQUIRE(dir_B);
CHECK(dir_B->is_dir());
CHECK(dir_B->is_locally_available());
CHECK(dir_B->is_unreachable());
auto file_X = files_local->by_name("dir-a/B/x.bin");
REQUIRE(file_X);
CHECK(file_X->is_file());
CHECK(!file_X->is_locally_available());
CHECK(file_X->is_unreachable());
auto dir_C = files_local->by_name("dir-a/C");
REQUIRE(dir_C);
CHECK(dir_C->is_dir());
CHECK(dir_C->is_locally_available());
}
}
int watcher_notifications = 0;
};
F().run();
}
void test_read_file_errors() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
bool process_cmd(fs::task::segment_iterator_t &task) noexcept override {
LOG_DEBUG(log, "process_cmd(segment_iterator_t) {}", narrow(task.path.generic_wstring()));
task.ec = std::make_error_code(std::errc::no_such_file_or_directory);
return true;
}
std::uint32_t get_hash_limit() override { return concurrency; }
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
concurrency = GENERATE(1, 5, 10, 100);
prepare(impl);
auto multiplier = GENERATE(1, 3, 5, 11);
auto block_sz = fs::block_sizes[0];
auto child = child_info_t{};
child.path = bfs::path("/some/path/file.bin");
child.status = bfs::file_status(bfs::file_type::regular);
child.size = block_sz * multiplier;
expect_dir_scan({child});
LOG_INFO(log, "triggering scan...");
builder->scan_start(folder_id).apply(*sup);
CHECK(files_local->size() == 0);
}
std::uint32_t concurrency = 1;
};
F().run();
};
void test_read_file_errors_partial() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
bool process_cmd(fs::task::segment_iterator_t &task) noexcept override {
static const constexpr size_t SZ = SHA256_DIGEST_LENGTH;
auto from = task.block_index;
auto to = from + task.block_count;
LOG_DEBUG(log, "process_cmd(segment_iterator_t) {}[{}..{}], error index = {}",
narrow(task.path.generic_wstring()), from, to, error_index);
if (error_index >= from && error_index < to) {
task.ec = std::make_error_code(std::errc::io_error);
} else {
for (std::int32_t i = from, j = 0; j < task.block_count; ++i, ++j) {
using digest_t = hasher::payload::digest_t;
auto bs = (j + 1 == task.block_count) ? task.last_block_size : task.block_size;
auto off = task.offset + std::int64_t{task.block_size} * j;
auto data = as_owned_bytes(std::string(fs::block_sizes[0], 'a' + i));
auto tmp_addr = sup->get_registry_address();
auto dst_addr = target->get_address();
auto digest = r::make_routed_message<digest_t>(tmp_addr, dst_addr, data, i, task.context);
auto digetst_backend = static_cast<hasher::message::digest_t *>(digest.get());
unsigned char d[SZ];
utils::digest(data.data(), data.size(), d);
digetst_backend->payload.result = utils::bytes_t(d, d + SZ);
sup->put(std::move(digest));
}
}
return true;
}
std::uint32_t get_hash_limit() override { return concurrency; }
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
concurrency = GENERATE(1, 2, 3, 4, 5);
error_index = GENERATE(0, 1, 2, 3, 4);
prepare(impl);
auto multiplier = 5;
auto block_sz = fs::block_sizes[0];
auto child = child_info_t{};
child.path = bfs::path("/some/path/file.bin");
child.status = bfs::file_status(bfs::file_type::regular);
child.size = block_sz * multiplier;
expect_dir_scan({child});
LOG_INFO(log, "triggering scan... concurrency: {}, error_index: {}", concurrency, error_index);
builder->scan_start(folder_id).apply(*sup);
CHECK(files_local->size() == 0);
}
std::uint32_t concurrency = 1;
std::uint32_t error_index = 0;
};
F().run();
};
void test_read_file_error_recovery() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
bool process_cmd(fs::task::segment_iterator_t &task) noexcept override {
static const constexpr size_t SZ = SHA256_DIGEST_LENGTH;
auto from = task.block_index;
auto to = from + task.block_count;
LOG_DEBUG(log, "process_cmd(segment_iterator_t) {}[{}..{}], error index = {}",
narrow(task.path.generic_wstring()), from, to, error_index);
if (error_index >= from && error_index < to) {
task.ec = std::make_error_code(std::errc::io_error);
} else {
for (std::int32_t i = from, j = 0; j < task.block_count; ++i, ++j) {
using digest_t = hasher::payload::digest_t;
auto bs = (j + 1 == task.block_count) ? task.last_block_size : task.block_size;
auto off = task.offset + std::int64_t{task.block_size} * j;
auto data = as_owned_bytes(std::string(fs::block_sizes[0], 'a' + i));
auto tmp_addr = sup->get_registry_address();
auto dst_addr = target->get_address();
auto digest = r::make_routed_message<digest_t>(tmp_addr, dst_addr, data, i, task.context);
auto digetst_backend = static_cast<hasher::message::digest_t *>(digest.get());
unsigned char d[SZ];
utils::digest(data.data(), data.size(), d);
digetst_backend->payload.result = utils::bytes_t(d, d + SZ);
sup->put(std::move(digest));
}
blocks_read += task.block_count;
}
return true;
}
std::uint32_t get_hash_limit() override { return concurrency; }
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
error_index = 3;
prepare(impl);
auto multiplier = 5;
auto block_sz = fs::block_sizes[0];
auto child = child_info_t{};
child.path = bfs::path("/some/path/file.bin");
child.status = bfs::file_status(bfs::file_type::regular);
child.size = block_sz * multiplier;
expect_dir_scan({child});
LOG_INFO(log, "triggering scan...");
builder->scan_start(folder_id).apply(*sup);
CHECK(files_local->size() == 0);
CHECK(blocks_read == 0);
error_index = 99;
expect_dir_scan({child});
LOG_INFO(log, "triggering scan...");
builder->scan_start(folder_id).apply(*sup);
CHECK(files_local->size() == 1);
auto file = files_local->begin()->get();
REQUIRE(file);
REQUIRE(file->get_size() == child.size);
REQUIRE(file->iterate_blocks().get_total() == 5);
CHECK(blocks_read == 5);
}
std::uint32_t concurrency = 4;
std::uint32_t error_index = 0;
std::uint32_t blocks_read = 0;
};
F().run();
};
void test_duplicates() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
auto file = proto::FileInfo();
auto file_name = std::string_view("some-file-name.bin");
proto::set_name(file, file_name);
proto::set_permissions(file, 0123);
proto::set_modified_s(file, 12345);
auto file_type = GENERATE(FT::DIRECTORY, FT::FILE, FT::SYMLINK);
proto::set_type(file, file_type);
if (file_type == FT::SYMLINK) {
proto::set_symlink_target(file, "/some/target");
}
mk_update(file, fs::update_type_t::created, false);
auto seq = folder_local->get_max_sequence();
mk_update(file, fs::update_type_t::created, false);
CHECK(folder_local->get_max_sequence() == seq);
proto::set_modified_s(file, 123456);
mk_update(file, fs::update_type_t::meta, false);
CHECK(folder_local->get_max_sequence() == seq + 1);
mk_update(file, fs::update_type_t::meta, false);
CHECK(folder_local->get_max_sequence() == seq + 1);
}
};
F().run();
}
void test_multi_folders_update() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using child_info_t = fs::task::scan_dir_t::child_info_t;
void main() noexcept override {
for (auto folder_id : {"p1", "p2", "p3"}) {
db::Folder db_folder;
db::set_id(db_folder, folder_id);
db::set_label(db_folder, folder_id);
db::set_path(db_folder, fmt::format("/some/{}", folder_id));
db::set_folder_type(db_folder, db::FolderType::send_and_receive);
db::set_watched(db_folder, true);
builder->upsert_folder(db_folder, 5).apply(*sup);
}
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
launch_target(impl);
expect_dir_scan({make_child("/some/p1/A")});
expect_dir_scan({make_child("/some/p1/A/a1"), make_child("/some/p1/A/a2"), make_child("/some/p1/A/a3")});
expect_dir_scan({});
expect_dir_scan({});
expect_dir_scan({});
expect_dir_scan({make_child("/some/p2/B")});
expect_dir_scan({make_child("/some/p2/B/b1"), make_child("/some/p2/B/b2"), make_child("/some/p2/B/b3")});
expect_dir_scan({});
expect_dir_scan({});
expect_dir_scan({});
expect_dir_scan({make_child("/some/p3/C")});
expect_dir_scan({make_child("/some/p3/C/c1"), make_child("/some/p3/C/c2"), make_child("/some/p3/C/c3")});
expect_dir_scan({});
expect_dir_scan({});
expect_dir_scan({});
struct D {
std::string_view folder_id;
std::initializer_list<std::string_view> names;
};
D changes[] = {{"p1", {"A", "A/a1", "A/a2", "A/a3"}},
{"p2", {"B", "B/b1", "B/b2", "B/b3"}},
{"p3", {"C", "C/c1", "C/c2", "C/c3"}}};
auto folder_changes = fs::payload::folder_changes_t{};
for (auto &c : changes) {
auto file_changes = fs::payload::file_changes_t{};
for (auto &name : c.names) {
auto file = proto::FileInfo();
proto::set_name(file, name);
proto::set_permissions(file, default_perms);
proto::set_modified_s(file, 12345);
proto::set_type(file, FT::DIRECTORY);
auto change = fs::payload::file_info_t(file, {}, fs::update_type_t::created, true);
file_changes.emplace_back(std::move(change));
}
auto folder_id = std::string(c.folder_id);
auto folder_change = fs::payload::folder_change_t{folder_id, std::move(file_changes)};
folder_changes.emplace_back(std::move(folder_change));
}
auto &addr = sup->get_address();
sup->send<fs::payload::folder_changes_t>(addr, std::move(folder_changes));
sup->do_process();
auto &folders_map = cluster->get_folders();
auto f1 = folders_map.by_id("p1");
auto f2 = folders_map.by_id("p2");
auto f3 = folders_map.by_id("p3");
auto &f1_files = f1->get_folder_infos().by_device(*local_device)->get_file_infos();
auto f_A = f1_files.by_name("A");
auto f_a1 = f1_files.by_name("A/a1");
auto f_a2 = f1_files.by_name("A/a2");
auto f_a3 = f1_files.by_name("A/a3");
CHECK(f_A);
CHECK(f_a1);
CHECK(f_a2);
CHECK(f_a3);
auto &f2_files = f2->get_folder_infos().by_device(*local_device)->get_file_infos();
auto f_B = f2_files.by_name("B");
auto f_b1 = f2_files.by_name("B/b1");
auto f_b2 = f2_files.by_name("B/b2");
auto f_b3 = f2_files.by_name("B/b3");
CHECK(f_B);
CHECK(f_b1);
CHECK(f_b2);
CHECK(f_b3);
auto &f3_files = f3->get_folder_infos().by_device(*local_device)->get_file_infos();
auto f_C = f3_files.by_name("C");
auto f_c1 = f3_files.by_name("C/c1");
auto f_c2 = f3_files.by_name("C/c2");
auto f_c3 = f3_files.by_name("C/c3");
CHECK(f_C);
CHECK(f_c1);
CHECK(f_c2);
CHECK(f_c3);
}
};
F().run();
}
void test_hierarchy_removal_order() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
for (auto &name : {"d0", "d0/d1", "d0/f1", "d0/d1/d2", "d0/d1/f2.1", "d0/d1/f2.2", "d0/d3"}) {
auto is_dir = std::string_view(name).find("f") == std::string::npos;
auto file = proto::FileInfo();
proto::set_name(file, name);
proto::set_type(file, is_dir ? FT::DIRECTORY : FT::FILE);
builder->local_update(folder_id, file).apply(*sup);
}
expect_dir_scan({});
LOG_INFO(log, "triggering scan...");
sup->updated_names.clear();
builder->scan_start(folder_id).apply(*sup);
// clang-format off
auto expected_names = my_supervisort_t::strings_t {
"d0/d1/d2",
"d0/d1/f2.1",
"d0/d1/f2.2",
"d0/d1",
"d0/d3",
"d0/f1",
"d0",
};
// clang-format on
CHECK(sup->updated_names == expected_names);
}
};
F().run();
}
void test_hierarchy_update_dirs_only() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
expect_dir_scan({make_child("/some/path/dir/subdir")});
expect_dir_scan({});
auto folder_changes = fs::payload::folder_changes_t{};
auto file_changes = fs::payload::file_changes_t{};
for (auto &name : {"dir", "dir/subdir"}) {
auto file = proto::FileInfo();
proto::set_name(file, name);
proto::set_permissions(file, default_perms);
proto::set_modified_s(file, 12345);
proto::set_type(file, FT::DIRECTORY);
auto change = fs::payload::file_info_t(file, {}, fs::update_type_t::created, true);
file_changes.emplace_back(std::move(change));
}
auto folder_change = fs::payload::folder_change_t{folder_id, std::move(file_changes)};
folder_changes.emplace_back(std::move(folder_change));
auto &addr = sup->get_address();
sup->send<fs::payload::folder_changes_t>(addr, std::move(folder_changes));
sup->do_process();
CHECK(files_local->size() == 2);
auto d = files_local->by_name("dir");
CHECK(d);
auto subdir = files_local->by_name("dir/subdir");
CHECK(subdir);
}
};
F().run();
}
void test_hierarchy_update_with_content() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
expect_dir_scan({make_child("/some/path/dir/file.bin", bfs::file_type::regular, 5)});
expect_dir_scan({});
expect_bytes_hash(as_bytes("12345"));
auto folder_changes = fs::payload::folder_changes_t{};
auto file_changes = fs::payload::file_changes_t{};
[&]() {
auto file = proto::FileInfo();
proto::set_name(file, "dir");
proto::set_permissions(file, default_perms);
proto::set_modified_s(file, 12345);
proto::set_type(file, FT::DIRECTORY);
auto change = fs::payload::file_info_t(file, {}, fs::update_type_t::created, true);
file_changes.emplace_back(std::move(change));
}();
[&]() {
auto file = proto::FileInfo();
proto::set_name(file, "dir/file.bin");
proto::set_permissions(file, default_perms);
proto::set_modified_s(file, 12345);
proto::set_type(file, FT::FILE);
proto::set_size(file, 5);
auto change = fs::payload::file_info_t(file, {}, fs::update_type_t::created, false);
file_changes.emplace_back(std::move(change));
}();
auto folder_change = fs::payload::folder_change_t{folder_id, std::move(file_changes)};
folder_changes.emplace_back(std::move(folder_change));
auto &addr = sup->get_address();
sup->send<fs::payload::folder_changes_t>(addr, std::move(folder_changes));
sup->do_process();
CHECK(files_local->size() == 2);
auto d = files_local->by_name("dir");
CHECK(d);
auto f = files_local->by_name("dir/file.bin");
REQUIRE(f);
CHECK(f->get_size() == 5);
CHECK(f->iterate_blocks().get_total() == 1);
CHECK(f->is_locally_available());
}
};
F().run();
}
void test_malformed_hierarchy_update() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
expect_dir_scan({make_child("/some/path/dir/file.bin", bfs::file_type::regular, 5)});
expect_dir_scan({});
expect_bytes_hash(as_bytes("12345"));
auto folder_changes = fs::payload::folder_changes_t{};
auto file_changes = fs::payload::file_changes_t{};
[&]() {
auto file = proto::FileInfo();
proto::set_name(file, "dir/file.bin");
proto::set_permissions(file, default_perms);
proto::set_modified_s(file, 12345);
proto::set_type(file, FT::FILE);
proto::set_size(file, 5);
auto change = fs::payload::file_info_t(file, {}, fs::update_type_t::created, false);
file_changes.emplace_back(std::move(change));
}();
auto folder_change = fs::payload::folder_change_t{folder_id, std::move(file_changes)};
folder_changes.emplace_back(std::move(folder_change));
auto &addr = sup->get_address();
sup->send<fs::payload::folder_changes_t>(addr, std::move(folder_changes));
sup->do_process();
CHECK(files_local->size() == 0);
}
};
F().run();
}
void test_scan_dirs_race_unix() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
bool process_cmd(fs::task::scan_dir_t &task) noexcept override {
if (task.path == bfs::path("/some/path/a")) {
LOG_INFO(log, "mix-in update");
auto pr_dir = proto::FileInfo();
proto::set_name(pr_dir, "x");
proto::set_permissions(pr_dir, default_perms);
proto::set_modified_s(pr_dir, 12345);
proto::set_type(pr_dir, FT::DIRECTORY);
mk_update(pr_dir, fs::update_type_t::created, true, false);
proto::set_name(pr_dir, "y");
mk_update(pr_dir, fs::update_type_t::created, true, false);
}
return parent_t::process_cmd(task);
}
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
expect_dir_scan({make_child("/some/path/a")});
expect_dir_scan({make_child("/some/path/a/b")});
expect_dir_scan({make_child("/some/path/a/b/c")});
expect_dir_scan({});
expect_dir_scan({make_child("/some/path/x/x1")});
expect_dir_scan({make_child("/some/path/x/x1/x2")});
expect_dir_scan({});
expect_dir_scan({make_child("/some/path/y/y1")});
expect_dir_scan({make_child("/some/path/y/y1/y2")});
expect_dir_scan({});
LOG_INFO(log, "triggering scan...");
builder->scan_start(folder_id).apply(*sup);
CHECK(files_local->size() == 9);
CHECK(folder_local->get_max_sequence() == 9);
}
};
F().run();
}
void test_scan_dirs_race_unix_2() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
expect_dir_scan({make_child("/some/path/a")});
expect_dir_scan({});
LOG_INFO(log, "triggering scan (1)...");
builder->scan_start(folder_id).apply(*sup);
REQUIRE(files_local->size() == 1);
auto pr_dir = proto::FileInfo();
proto::set_name(pr_dir, "x");
proto::set_permissions(pr_dir, default_perms);
proto::set_modified_s(pr_dir, 12345);
proto::set_type(pr_dir, FT::DIRECTORY);
mk_update(pr_dir, fs::update_type_t::created, true, false);
proto::set_name(pr_dir, "y");
mk_update(pr_dir, fs::update_type_t::created, true, false);
expect_dir_scan({});
expect_dir_scan({});
expect_dir_scan({make_child("/some/path/a"), make_child("/some/path/x"), make_child("/some/path/y")});
expect_dir_scan({});
expect_dir_scan({});
expect_dir_scan({make_child("/some/path/a/b")});
expect_dir_scan({make_child("/some/path/a/b/c")});
expect_dir_scan({});
LOG_INFO(log, "triggering scan (2)...");
builder->scan_start(folder_id).apply(*sup);
CHECK(files_local->size() == 5);
CHECK(folder_local->get_max_sequence() == 5);
}
};
F().run();
}
void test_scan_dirs_race_win32() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
bool process_cmd(fs::task::scan_dir_t &task) noexcept override {
if (task.path == bfs::path("/some/path/a")) {
LOG_INFO(log, "mix-in update");
auto pr_dir = proto::FileInfo();
proto::set_permissions(pr_dir, default_perms);
proto::set_modified_s(pr_dir, 12345);
proto::set_type(pr_dir, FT::DIRECTORY);
for (auto name : {"x", "x/x1", "x/x1/x2", "y", "y/y1", "y/y1/y2"}) {
proto::set_name(pr_dir, name);
mk_update(pr_dir, fs::update_type_t::created, false, false);
}
}
return parent_t::process_cmd(task);
}
void main() noexcept override {
prepare(I::win32);
expect_dir_scan({make_child("/some/path/a")});
expect_dir_scan({make_child("/some/path/a/b")});
expect_dir_scan({make_child("/some/path/a/b/c")});
expect_dir_scan({});
LOG_INFO(log, "triggering scan...");
builder->scan_start(folder_id).apply(*sup);
CHECK(files_local->size() == 9);
CHECK(folder_local->get_max_sequence() == 9);
}
};
F().run();
}
void test_scan_dirs_race_win32_2() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
prepare(I::win32);
expect_dir_scan({make_child("/some/path/a")});
expect_dir_scan({make_child("/some/path/a/b")});
expect_dir_scan({make_child("/some/path/a/b/c")});
expect_dir_scan({});
LOG_INFO(log, "triggering scan (1)...");
builder->scan_start(folder_id).apply(*sup);
REQUIRE(files_local->size() == 3);
auto pr_dir = proto::FileInfo();
proto::set_name(pr_dir, "x");
proto::set_permissions(pr_dir, default_perms);
proto::set_modified_s(pr_dir, 12345);
proto::set_type(pr_dir, FT::DIRECTORY);
mk_update(pr_dir, fs::update_type_t::created, false, false);
proto::set_name(pr_dir, "y");
mk_update(pr_dir, fs::update_type_t::created, false, false);
expect_dir_scan({make_child("/some/path/a"), make_child("/some/path/x"), make_child("/some/path/y")});
expect_dir_scan({});
expect_dir_scan({});
expect_dir_scan({make_child("/some/path/a/b")});
expect_dir_scan({make_child("/some/path/a/b/c")});
expect_dir_scan({});
LOG_INFO(log, "triggering scan (2)...");
builder->scan_start(folder_id).apply(*sup);
CHECK(files_local->size() == 5);
CHECK(folder_local->get_max_sequence() == 5);
}
};
F().run();
}
void test_hashing_race() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using trigger_t = std::function<void()>;
bool process_cmd(fs::task::segment_iterator_t &task) noexcept override {
auto r = parent_t::process_cmd(task);
if (trigger) {
trigger();
trigger = {};
}
return r;
}
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
auto number = std::uint32_t{5};
auto sz = fs::block_sizes[0] * number;
expect_dir_scan({make_child("/some/path/file.bin", bfs::file_type::regular, sz, default_perms, 12345)});
auto block_a = as_owned_bytes(std::string(fs::block_sizes[0], 'a'));
auto block_b = as_owned_bytes(std::string(fs::block_sizes[0], 'b'));
auto hash_a = utils::sha256_digest(block_a).value();
auto hash_b = utils::sha256_digest(block_b).value();
SECTION("scan, then update") {
for (std::uint32_t i = 0; i < number; ++i) {
expect_bytes_hash(block_a);
}
for (std::uint32_t i = 0; i < number; ++i) {
expect_bytes_hash(block_b);
}
LOG_INFO(log, "triggering scan...");
trigger = [&]() {
LOG_INFO(log, "triggering update...");
auto pr_file = proto::FileInfo();
proto::set_name(pr_file, "file.bin");
proto::set_permissions(pr_file, default_perms);
proto::set_modified_s(pr_file, 12348);
proto::set_type(pr_file, FT::FILE);
proto::set_size(pr_file, sz);
mk_update(pr_file, fs::update_type_t::content, false, false);
};
builder->scan_start(folder_id).apply(*sup);
CHECK(folder_local->get_max_sequence() == 2);
}
SECTION("update, then scan") {
for (std::uint32_t i = 0; i < number; ++i) {
expect_bytes_hash(block_b);
}
LOG_INFO(log, "triggering update...");
auto pr_file = proto::FileInfo();
proto::set_name(pr_file, "file.bin");
proto::set_permissions(pr_file, default_perms);
proto::set_modified_s(pr_file, 12345);
proto::set_type(pr_file, FT::FILE);
proto::set_size(pr_file, sz);
mk_update(pr_file, fs::update_type_t::content, false, false);
builder->scan_start(folder_id).apply(*sup);
CHECK(folder_local->get_max_sequence() == 1);
}
REQUIRE(files_local->size() == 1);
auto f = files_local->by_name("file.bin");
REQUIRE(f);
REQUIRE(f->get_size() == sz);
REQUIRE(f->iterate_blocks().get_total() == number);
for (auto it = f->iterate_blocks(); it.current().first; it.next()) {
auto b = it.current().first->get_hash();
CHECK(b == hash_b);
}
CHECK(hashed_blocks.size() == 0);
}
trigger_t trigger;
};
F().run();
}
void test_renaming_simple() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using trigger_t = std::function<void()>;
void main() noexcept override {
prepare(I::inotify);
{
auto dir = proto::FileInfo();
proto::set_name(dir, narrow(L"папка"));
proto::set_permissions(dir, default_perms);
proto::set_modified_s(dir, 12345);
proto::set_type(dir, FT::DIRECTORY);
builder->local_update(folder_id, dir).apply(*sup);
}
auto file_name_1 = narrow(L"папка/файл-1.bin");
auto file_name_2 = narrow(L"папка/файл-2.bin");
auto file = proto::FileInfo();
proto::set_permissions(file, default_perms);
proto::set_modified_s(file, 12345);
proto::set_name(file, file_name_1);
SECTION("empty dir/file/link") {
auto file_type = GENERATE(FT::DIRECTORY, FT::FILE, FT::SYMLINK);
proto::set_type(file, file_type);
if (file_type == FT::SYMLINK) {
proto::set_symlink_target(file, "/some/target");
}
if (file_type == FT::DIRECTORY) {
expect_dir_scan({});
}
builder->local_update(folder_id, file).apply(*sup);
proto::set_name(file, file_name_2);
auto seq_1 = folder_local->get_max_sequence();
make_update_rename(file, file_name_1);
CHECK(folder_local->get_max_sequence() > seq_1);
auto f2 = files_local->by_name(file_name_2);
REQUIRE(f2);
CHECK(f2->get_permissions() == default_perms);
CHECK(f2->get_modified_s() == 12345);
CHECK(!f2->is_deleted());
CHECK(model::file_info_t::as_type(f2->get_type()) == file_type);
auto f1 = files_local->by_name(file_name_1);
REQUIRE(f1);
CHECK(f1->get_permissions() == default_perms);
CHECK(f1->get_modified_s() == 12345);
CHECK(f1->is_deleted());
}
SECTION("non-empty file") {
proto::set_type(file, FT::FILE);
auto data = as_bytes("12345");
auto data_h = utils::sha256_digest(as_bytes("12345")).value();
auto b = proto::BlockInfo();
proto::set_hash(b, data_h);
proto::set_offset(b, 0);
proto::set_size(b, 5);
proto::set_size(file, 5);
proto::add_blocks(file, b);
builder->local_update(folder_id, file).apply(*sup);
auto &blocks = cluster->get_blocks();
REQUIRE(blocks.size() == 1);
proto::set_name(file, file_name_2);
auto seq_1 = folder_local->get_max_sequence();
make_update_rename(file, file_name_1);
CHECK(folder_local->get_max_sequence() > seq_1);
auto f2 = files_local->by_name(file_name_2);
REQUIRE(f2);
CHECK(f2->get_permissions() == default_perms);
CHECK(f2->get_modified_s() == 12345);
CHECK(!f2->is_deleted());
CHECK(model::file_info_t::as_type(f2->get_type()) == FT::FILE);
CHECK(f2->get_block_size() == 5);
CHECK(f2->iterate_blocks(0).get_total() == 1);
CHECK(f2->iterate_blocks(0).current().first->get_hash() == data_h);
auto f1 = files_local->by_name(file_name_1);
REQUIRE(f1);
CHECK(f1->get_permissions() == default_perms);
CHECK(f1->get_modified_s() == 12345);
CHECK(f1->is_deleted());
CHECK(blocks.size() == 1);
}
}
};
F().run();
}
void test_renaming_hierarchy() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using trigger_t = std::function<void()>;
void main() noexcept override {
prepare(I::inotify);
auto data_1 = as_bytes("12345");
auto data1_h = utils::sha256_digest(data_1).value();
auto data_2 = as_bytes("67890");
auto data2_h = utils::sha256_digest(data_2).value();
auto data_3 = as_bytes("55555");
auto data3_h = utils::sha256_digest(data_3).value();
auto b1 = proto::BlockInfo();
proto::set_hash(b1, data1_h);
proto::set_offset(b1, 0);
proto::set_size(b1, 5);
auto b2 = proto::BlockInfo();
proto::set_hash(b2, data2_h);
proto::set_offset(b2, 0);
proto::set_size(b2, 5);
auto b3 = proto::BlockInfo();
proto::set_hash(b3, data2_h);
proto::set_offset(b3, 5);
proto::set_size(b3, 5);
{
auto pr = proto::FileInfo();
proto::set_permissions(pr, default_perms);
proto::set_modified_s(pr, 12345);
proto::set_type(pr, FT::DIRECTORY);
auto names = {
L"папка", L"папка/sd", L"папка/sd/f1.bin", L"папка/sd/f2.bin", L"папка/kept",
};
for (auto &name_w : names) {
auto name = narrow(name_w);
proto::set_name(pr, name);
proto::clear_blocks(pr);
if (name.find("f1") != std::string::npos) {
proto::set_type(pr, FT::FILE);
proto::set_size(pr, 5);
proto::set_block_size(pr, 5);
proto::add_blocks(pr, b1);
}
if (name.find("f2") != std::string::npos) {
proto::set_type(pr, FT::FILE);
proto::set_size(pr, 10);
proto::set_block_size(pr, 5);
proto::add_blocks(pr, b2);
proto::add_blocks(pr, b3);
}
builder->local_update(folder_id, pr).apply(*sup);
}
}
auto file_name_1 = narrow(L"папка/sd");
auto file_name_2 = narrow(L"папка/подпапка");
auto pr = proto::FileInfo();
proto::set_permissions(pr, default_perms);
proto::set_modified_s(pr, 12345);
proto::set_type(pr, FT::DIRECTORY);
proto::set_name(pr, file_name_2);
make_update_rename(pr, file_name_1);
auto ex_names = {L"папка/sd", L"папка/sd/f1.bin", L"папка/sd/f2.bin"};
for (auto &name_w : ex_names) {
auto name = narrow(name_w);
auto f = files_local->by_name(name);
REQUIRE(f);
log->debug("f = {}", f->get_name()->get_full_name());
CHECK(f->is_deleted());
}
auto exising_names = {L"папка", L"папка/подпапка", L"папка/подпапка/f1.bin", L"папка/подпапка/f2.bin",
L"папка/kept"};
for (auto &name_w : exising_names) {
auto name = narrow(name_w);
log->debug("f = {}", name);
auto f = files_local->by_name(name);
REQUIRE(f);
CHECK(!f->is_deleted());
}
}
};
F().run();
}
void test_renaming_race() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
using trigger_t = std::function<void()>;
using rename_pair_t = std::pair<std::string_view, std::string_view>;
void main() noexcept override {
prepare(I::inotify);
{
auto pr = proto::FileInfo();
proto::set_permissions(pr, default_perms);
proto::set_modified_s(pr, 12345);
proto::set_type(pr, FT::DIRECTORY);
auto names = {"dir_a0", "dir_a0/b0", "dir_a0/c0"};
for (auto &name : names) {
proto::set_name(pr, name);
builder->local_update(folder_id, pr).apply(*sup);
}
}
CHECK(folder_local->get_max_sequence() == 3);
// clang-format off
auto pairs = {
rename_pair_t{"dir_a0/b0", "dir_a0/b1"},
rename_pair_t{"dir_a0", "dir_a2"},
rename_pair_t{"dir_a2/c0", "dir_a2/c3"},
rename_pair_t{"dir_a2/b1", "dir_a2/b4"},
};
// clang-format on
auto changes = fs::payload::file_changes_t{};
for (auto &[from, to] : pairs) {
auto pr = proto::FileInfo();
proto::set_permissions(pr, default_perms);
proto::set_modified_s(pr, 12345);
proto::set_type(pr, FT::DIRECTORY);
proto::set_name(pr, to);
auto change =
fs::payload::file_info_t(std::move(pr), std::string(from), fs::update_type_t::meta, false);
changes.push_back(std::move(change));
}
auto folder_change = fs::payload::folder_change_t{folder_id, std::move(changes)};
auto folder_changes = fs::payload::folder_changes_t{{std::move(folder_change)}};
auto &addr = sup->get_address();
sup->send<fs::payload::folder_changes_t>(addr, std::move(folder_changes));
sup->do_process();
CHECK(folder_local->get_max_sequence() == 15);
auto deleted = {"dir_a0/b0", "dir_a0/c0", "dir_a0/b1", "dir_a0", "dir_a2/c0", "dir_a2/b1"};
auto prev_seq = std::int64_t{4};
for (auto &name : deleted) {
auto f = files_local->by_name(name);
log->debug("f = {}, seq = {}", name, f->get_sequence());
REQUIRE(f);
CHECK(f->is_deleted());
CHECK(prev_seq < f->get_sequence());
prev_seq = f->get_sequence();
}
auto existing = {"dir_a2", "dir_a2/b4", "dir_a2/c3"};
for (auto &name : existing) {
auto f = files_local->by_name(name);
log->debug("f = {}", name);
REQUIRE(f);
CHECK(!f->is_deleted());
}
}
};
F().run();
}
void test_avoid_dir_rescan() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
bool process_cmd(fs::task::scan_dir_t &task) noexcept override {
++dir_scans;
return parent_t::process_cmd(task);
}
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
expect_dir_scan({make_child("/some/path/dir/a-file")});
expect_dir_scan({});
auto file = proto::FileInfo();
auto file_name = std::string_view("dir");
proto::set_name(file, file_name);
proto::set_permissions(file, default_perms);
proto::set_modified_s(file, 12345);
proto::set_type(file, FT::DIRECTORY);
SECTION("within the same update") {
auto change = fs::payload::file_info_t(file, {}, fs::update_type_t::created, true);
auto changes = fs::payload::file_changes_t{change, change};
auto folder_change = fs::payload::folder_change_t{folder_id, std::move(changes)};
auto folder_changes = fs::payload::folder_changes_t{{std::move(folder_change)}};
auto &addr = sup->get_address();
LOG_DEBUG(log, "making update...");
sup->send<fs::payload::folder_changes_t>(addr, std::move(folder_changes));
sup->do_process();
}
SECTION("spread across different updates") {
mk_update(file, fs::update_type_t::created, true, false);
mk_update(file, fs::update_type_t::created, true);
}
CHECK(files_local->size() == 2);
REQUIRE(files_local->by_name("dir"));
REQUIRE(files_local->by_name("dir/a-file"));
CHECK(folder_local->get_max_sequence() == 2);
CHECK(dir_scans == 2);
}
int dir_scans = 0;
};
F().run();
}
void test_no_pending_io() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
bool process_cmd(fs::task::scan_dir_t &task) noexcept override { return parent_t::process_cmd(task); }
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
auto file = proto::FileInfo();
auto file_name = std::string_view("a-file");
proto::set_name(file, file_name);
proto::set_permissions(file, default_perms);
proto::set_modified_s(file, 12345);
proto::set_type(file, FT::FILE);
proto::set_size(file, 5);
proto::set_block_size(file, 5);
auto data = as_owned_bytes("12345");
auto data_h = utils::sha256_digest(data).value();
auto &b1 = proto::add_blocks(file);
proto::set_hash(b1, data_h);
proto::set_size(b1, data.size());
auto change = fs::payload::file_info_t(file, {}, fs::update_type_t::created, false);
auto changes = fs::payload::file_changes_t{change, change};
auto folder_change = fs::payload::folder_change_t{folder_id, std::move(changes)};
auto folder_changes = fs::payload::folder_changes_t{{std::move(folder_change)}};
auto &addr = sup->get_address();
builder->local_update(folder_id, file).apply(*sup);
LOG_DEBUG(log, "making update...");
sup->send<fs::payload::folder_changes_t>(addr, std::move(folder_changes));
sup->do_process();
CHECK(files_local->size() == 1);
CHECK(folder_local->get_max_sequence() == 1);
auto f = files_local->by_name("a-file");
REQUIRE(f);
CHECK(f->get_size() == 5);
}
};
F().run();
}
void test_double_content_update() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
bool process_cmd(fs::task::scan_dir_t &task) noexcept override { return parent_t::process_cmd(task); }
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
auto file = proto::FileInfo();
auto file_name = std::string_view("a-file");
proto::set_name(file, file_name);
proto::set_permissions(file, default_perms);
proto::set_modified_s(file, 12345);
proto::set_type(file, FT::FILE);
proto::set_size(file, 5);
proto::set_block_size(file, 5);
auto data = as_owned_bytes("12345");
auto data_h = utils::sha256_digest(data).value();
auto &b1 = proto::add_blocks(file);
proto::set_hash(b1, data_h);
proto::set_size(b1, data.size());
auto change_1 = fs::payload::file_info_t(file, {}, fs::update_type_t::created, false);
auto change_2 = fs::payload::file_info_t(file, {}, fs::update_type_t::content, false);
auto changes = fs::payload::file_changes_t{change_1, change_2};
auto folder_change = fs::payload::folder_change_t{folder_id, std::move(changes)};
auto folder_changes = fs::payload::folder_changes_t{{std::move(folder_change)}};
auto &addr = sup->get_address();
expect_bytes_hash(data);
LOG_DEBUG(log, "making update...");
sup->send<fs::payload::folder_changes_t>(addr, std::move(folder_changes));
sup->do_process();
CHECK(files_local->size() == 1);
CHECK(folder_local->get_max_sequence() == 1);
auto f = files_local->by_name("a-file");
REQUIRE(f);
CHECK(f->get_size() == 5);
}
};
F().run();
}
void test_dir_scan_and_hashing_race() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
bool process_cmd(fs::task::segment_iterator_t &task) noexcept override {
if (task.path == "/some/path/dir/f2.bin") {
auto file = proto::FileInfo();
auto file_name = std::string_view("dir/f1.bin");
proto::set_name(file, file_name);
proto::set_permissions(file, default_perms);
proto::set_type(file, FT::FILE);
proto::set_size(file, 5);
proto::set_block_size(file, 5);
auto data = as_owned_bytes("67890");
auto data_h = utils::sha256_digest(data).value();
auto &b1 = proto::add_blocks(file);
proto::set_hash(b1, data_h);
proto::set_size(b1, data.size());
auto diff = builder->local_update(folder_id, file).extract();
sup->send<model::payload::model_update_t>(sup->get_address(), std::move(diff), nullptr);
}
return parent_t::process_cmd(task);
}
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue);
prepare(impl);
expect_dir_scan({make_child("/some/path/dir/f1.bin", bfs::file_type::regular, 5),
make_child("/some/path/dir/f2.bin", bfs::file_type::regular, 5)});
expect_dir_scan({});
auto file = proto::FileInfo();
auto file_name = std::string_view("dir");
proto::set_name(file, file_name);
proto::set_permissions(file, default_perms);
proto::set_type(file, FT::DIRECTORY);
expect_bytes_hash(as_bytes("12345"));
mk_update(file, fs::update_type_t::created, true);
CHECK(files_local->size() == 3);
REQUIRE(files_local->by_name("dir"));
REQUIRE(files_local->by_name("dir/f1.bin"));
REQUIRE(files_local->by_name("dir/f2.bin"));
CHECK(folder_local->get_max_sequence() == 3);
}
};
F().run();
}
void test_invalid_utf8_names() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
auto invalid = std::uint8_t{201};
auto ptr = reinterpret_cast<char *>(&invalid);
auto invalid_name = std::string_view(ptr, ptr + 1);
REQUIRE(!utils::is_utf8_valid(invalid_name));
auto file = proto::FileInfo();
SECTION("update regular") {
proto::set_name(file, invalid_name);
proto::set_type(file, FT::FILE);
mk_update(file, fs::update_type_t::created, true);
CHECK(files_local->size() == 0);
}
#ifndef SYNCSPIRIT_WIN
SECTION("update symlink") {
proto::set_name(file, "symlink");
proto::set_symlink_target(file, invalid_name);
proto::set_type(file, FT::SYMLINK);
mk_update(file, fs::update_type_t::created, true);
CHECK(files_local->size() == 0);
}
SECTION("in dir content") {
auto children = child_infos_t{make_child(invalid_name, bfs::file_type::regular)};
expect_dir_scan(children);
builder->scan_start(folder_id).apply(*sup);
CHECK(files_local->size() == 0);
}
#endif
}
};
F().run();
}
void test_rm_folder_on_scan() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void on_exec(fs::message::foreign_executor_t &msg) override {
auto diff = builder->remove_folder(*folder).extract();
sup->send<model::payload::model_update_t>(sup->get_address(), std::move(diff));
folder.reset();
folder_local.reset();
parent_t::on_exec(msg);
}
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
auto children = child_infos_t{make_child("a-file", bfs::file_type::regular)};
expect_dir_scan(children);
builder->scan_start(folder_id).apply(*sup);
}
};
F().run();
}
void test_not_ye_scanned_parent() {
struct F : folder_fixture_t {
using parent_t = folder_fixture_t;
using parent_t::parent_t;
void main() noexcept override {
auto impl = GENERATE(I::inotify, I::kqueue, I::win32);
prepare(impl);
auto pr_dir = proto::FileInfo();
proto::set_name(pr_dir, "dir");
proto::set_permissions(pr_dir, default_perms);
proto::set_modified_s(pr_dir, 12345);
proto::set_type(pr_dir, proto::FileInfoType::DIRECTORY);
builder->local_update(folder_id, pr_dir).apply(*sup);
REQUIRE(files_local->size() == 1);
auto dir = files_local->by_name("dir");
dir->mark_local(false);
auto pr_file = proto::FileInfo();
proto::set_name(pr_file, "dir/file.bin");
proto::set_type(pr_file, FT::FILE);
proto::set_size(pr_file, 5);
mk_update(pr_file, fs::update_type_t::created, true);
CHECK(files_local->size() == 1);
}
};
F().run();
}
int _init() {
test::init_logging();
REGISTER_TEST_CASE(test_just_start, "test_just_start", "[fs]");
REGISTER_TEST_CASE(test_watch_unwatch, "test_watch_unwatch", "[fs]");
REGISTER_TEST_CASE(test_trivial_changes, "test_trivial_changes", "[fs]");
REGISTER_TEST_CASE(test_ignoring_tmps, "test_ignoring_tmps", "[fs]");
REGISTER_TEST_CASE(test_hashing, "test_hashing", "[fs]");
REGISTER_TEST_CASE(test_rescan, "test_rescan", "[fs]");
REGISTER_TEST_CASE(test_skip_scan_known, "test_skip_scan_known", "[fs]");
REGISTER_TEST_CASE(test_new_dir_refinement, "test_new_dir_refinement", "[fs]");
REGISTER_TEST_CASE(test_new_dir_without_refinement, "test_new_dir_without_refinement", "[fs]");
REGISTER_TEST_CASE(test_remove_dir_refinement, "test_remove_dir_refinement", "[fs]");
REGISTER_TEST_CASE(test_scan_notification_unix, "test_scan_notification_unix", "[fs]");
REGISTER_TEST_CASE(test_kqueue_changes, "test_kqueue_changes", "[fs]");
REGISTER_TEST_CASE(test_dir_scan_errors, "test_dir_scan_errors", "[fs]");
REGISTER_TEST_CASE(test_read_file_errors, "test_read_file_errors", "[fs]");
REGISTER_TEST_CASE(test_read_file_errors_partial, "test_read_file_errors_partial", "[fs]");
REGISTER_TEST_CASE(test_read_file_error_recovery, "test_read_file_error_recovery", "[fs]");
REGISTER_TEST_CASE(test_duplicates, "test_duplicates", "[fs]");
REGISTER_TEST_CASE(test_multi_folders_update, "test_multi_folders_update", "[fs]");
REGISTER_TEST_CASE(test_hierarchy_removal_order, "test_hierarchy_removal_order", "[fs]");
REGISTER_TEST_CASE(test_hierarchy_update_dirs_only, "test_hierarchy_update_dirs_only", "[fs]");
REGISTER_TEST_CASE(test_hierarchy_update_with_content, "test_hierarchy_update_with_content", "[fs]");
REGISTER_TEST_CASE(test_malformed_hierarchy_update, "test_malformed_hierarchy_update", "[fs]");
REGISTER_TEST_CASE(test_scan_dirs_race_unix, "test_scan_dirs_race_unix", "[fs]");
REGISTER_TEST_CASE(test_scan_dirs_race_unix_2, "test_scan_dirs_race_unix_2", "[fs]");
REGISTER_TEST_CASE(test_scan_dirs_race_win32, "test_scan_dirs_race_win32", "[fs]");
REGISTER_TEST_CASE(test_scan_dirs_race_win32_2, "test_scan_dirs_race_win32_2", "[fs]");
REGISTER_TEST_CASE(test_hashing_race, "test_hashing_race", "[fs]");
REGISTER_TEST_CASE(test_renaming_simple, "test_renaming_simple", "[fs]");
REGISTER_TEST_CASE(test_renaming_hierarchy, "test_renaming_hierarchy", "[fs]");
REGISTER_TEST_CASE(test_renaming_race, "test_renaming_race", "[fs]");
REGISTER_TEST_CASE(test_avoid_dir_rescan, "test_avoid_dir_rescan", "[fs]");
REGISTER_TEST_CASE(test_no_pending_io, "test_no_pending_io", "[fs]");
REGISTER_TEST_CASE(test_double_content_update, "test_double_content_update", "[fs]");
REGISTER_TEST_CASE(test_dir_scan_and_hashing_race, "test_dir_scan_and_hashing_race", "[fs]");
REGISTER_TEST_CASE(test_invalid_utf8_names, "test_invalid_utf8_names", "[fs]");
REGISTER_TEST_CASE(test_rm_folder_on_scan, "test_rm_folder_on_scan", "[fs]");
REGISTER_TEST_CASE(test_not_ye_scanned_parent, "test_not_ye_scanned_parent", "[fs]");
return 1;
}
static int v = _init();