Files
syncspirit/tests/087-local_keeper.cpp

2308 lines
97 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025-2026 Ivan Baidakou
#include "access.h"
#include "diff-builder.h"
#include "config/fs.h"
#include "hasher/hasher_plugin.h"
#include "constants.h"
#include "fs/fs_proxy.h"
#include "fs/fs_slave.h"
#include "fs/messages.h"
#include "fs/utils.h"
#include "fs/updates_mediator.h"
#include "managed_hasher.h"
#include "model/cluster.h"
#include "model/diff/advance/local_update.h"
#include "model/diff/local/file_availability.h"
#include "model/diff/load/interrupt.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 "presentation/folder_entity.h"
#include <chrono>
#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;
using boost::nowide::widen;
using task_processor_t = std::function<void(fs::fs_slave_t *)>;
struct executor_t : r::actor_base_t {
using parent_t = r::actor_base_t;
using parent_t::parent_t;
// clang-format off
using plugins_list_t = std::tuple<
r::plugin::address_maker_plugin_t,
r::plugin::lifetime_plugin_t,
r::plugin::init_shutdown_plugin_t,
r::plugin::link_server_plugin_t,
r::plugin::link_client_plugin_t,
hasher::hasher_plugin_t,
r::plugin::resources_plugin_t,
r::plugin::starter_plugin_t
>;
// clang-format on
void configure(r::plugin::plugin_base_t &plugin) noexcept {
r::actor_base_t::configure(plugin);
plugin.with_casted<hasher::hasher_plugin_t>([&](auto &p) {
hasher = &p;
p.configure_hashers(1);
});
}
hasher_plugin_t *hasher;
};
using executor_ptr_t = r::intrusive_ptr_t<executor_t>;
struct fixture_t;
struct my_supervisort_t : supervisor_t {
using parent_t = supervisor_t;
using parent_t::parent_t;
fixture_t *fixture = nullptr;
void on_model_update(model::message::model_update_t &) noexcept override;
outcome::result<void> operator()(const model::diff::advance::local_update_t &, void *) noexcept override;
outcome::result<void> operator()(const model::diff::local::file_availability_t &, void *) noexcept override;
};
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>;
fixture_t(bool auto_launch_ = true) noexcept
: root_path{unique_path()}, path_guard{root_path}, auto_launch{auto_launch_} {
bfs::create_directory(root_path);
mediator = new fs::updates_mediator_t(pt::microseconds{1});
}
virtual std::uint32_t get_hash_limit() { return 1; }
bool execute_slave(fs::payload::foreign_executor_t &slave) {
static constexpr auto retension = pt::milliseconds{1};
auto deadline = pt::microsec_clock::local_time() + retension;
auto fs_proxy = fs_proxy_t(*mediator, deadline);
auto ctx = execution_context_t();
ctx.fs_proxy = &fs_proxy;
ctx.plugin = executor->hasher;
return slave.exec(ctx);
}
virtual void execute(fs::message::foreign_executor_t &req) noexcept {
auto slave = static_cast<fs::fs_slave_t *>(req.payload.get());
slave->ec = {};
sup->log->info("executing foreign task: {}", execute_slave(*req.payload));
}
virtual void create_dir(fs::message::create_dir_t &req) noexcept {
auto &path = req.payload;
sup->log->info("on_create_dir, '{}'", narrow(path.wstring()));
bfs::create_directory(path, path.ec);
}
virtual void launch_hasher() noexcept {
hasher = sup->create_actor<managed_hasher_t>().index(1).auto_reply(true).timeout(timeout).finish().get();
}
virtual void on_diff(const model::diff::advance::local_update_t &) noexcept {}
virtual void on_diff(const model::diff::local::file_availability_t &) noexcept {}
virtual void on_model_update(model::message::model_update_t &) noexcept {}
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();
my_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(my_device, 1);
cluster->get_devices().put(my_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()); });
plugin.template with_casted<r::plugin::starter_plugin_t>([&](auto &p) {
using exec_msg_t = fs::message::foreign_executor_t;
using create_dir_msg_t = fs::message::create_dir_t;
p.subscribe_actor(r::lambda<exec_msg_t>([&](exec_msg_t &req) { execute(req); }));
p.subscribe_actor(r::lambda<create_dir_msg_t>([&](create_dir_msg_t &req) { create_dir(req); }));
});
};
auto folder_id = "1234-5678";
sup->start();
sup->do_process();
builder = std::make_unique<diff_builder_t>(*cluster);
builder->upsert_folder(folder_id, root_path)
.apply(*sup)
.share_folder(peer_id.get_sha256(), folder_id)
.apply(*sup);
folder = cluster->get_folders().by_id(folder_id);
folder_info = folder->get_folder_infos().by_device(*my_device);
files = &folder_info->get_file_infos();
folder_info_peer = folder->get_folder_infos().by_device(*peer_device);
files_peer = &folder_info_peer->get_file_infos();
CHECK(static_cast<r::actor_base_t *>(sup.get())->access<to::state>() == r::state_t::OPERATIONAL);
executor = sup->create_actor<executor_t>().timeout(timeout).finish();
launch_hasher();
sup->do_process();
auto fs_config = config::fs_config_t{3600, 10};
if (auto_launch) {
launch_target();
}
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() {
target = sup->create_actor<net::local_keeper_t>()
.timeout(timeout)
.sequencer(sequencer)
.concurrent_hashes(get_hash_limit())
.finish();
sup->do_process();
sup->send<syncspirit::model::payload::thread_ready_t>(sup->get_address(), cluster, std::this_thread::get_id());
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<supervisor_t> sup;
executor_ptr_t executor;
managed_hasher_t *hasher;
cluster_ptr_t cluster;
device_ptr_t my_device;
bfs::path root_path;
test::path_guard_t path_guard;
target_ptr_t target;
updates_mediator_ptr_t mediator;
model::folder_ptr_t folder;
model::folder_info_ptr_t folder_info;
model::folder_info_ptr_t folder_info_peer;
model::file_infos_map_t *files;
model::file_infos_map_t *files_peer;
model::device_ptr_t peer_device;
model::sequencer_ptr_t sequencer;
bool auto_launch;
};
auto my_supervisort_t::operator()(const model::diff::advance::local_update_t &diff, void *custom) noexcept
-> outcome::result<void> {
fixture->on_diff(diff);
return parent_t::operator()(diff, custom);
}
auto my_supervisort_t::operator()(const model::diff::local::file_availability_t &diff, void *custom) noexcept
-> outcome::result<void> {
fixture->on_diff(diff);
return parent_t::operator()(diff, custom);
}
void my_supervisort_t::on_model_update(model::message::model_update_t &diff) noexcept {
fixture->on_model_update(diff);
return parent_t::on_model_update(diff);
}
void test_simple() {
struct F : fixture_t {
std::uint32_t get_hash_limit() override { return 2; }
void main() noexcept override {
sys::error_code ec;
auto &blocks = cluster->get_blocks();
auto my_short_id = my_device->device_id().get_uint();
SECTION("root folder errors") {
auto dir_path = root_path / "some-dir";
folder->set_path(dir_path);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->is_suspended());
REQUIRE(folder->get_suspend_reason().message() != "");
SECTION("scan on created again") {
bfs::create_directories(dir_path);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(!folder->is_suspended());
REQUIRE(!folder->get_suspend_reason());
}
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
}
SECTION("emtpy root dir") {
auto dir_path = root_path / "some-dir";
bfs::create_directories(dir_path);
folder->set_path(dir_path);
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
}
SECTION("new items") {
SECTION("new dir") {
auto dir_path = root_path / "some-dir";
bfs::create_directories(dir_path);
builder->scan_start(folder->get_id()).apply(*sup);
auto file = files->by_name("some-dir");
REQUIRE(file);
CHECK(file->is_locally_available());
CHECK(file->is_dir());
CHECK(!file->is_link());
CHECK(file->get_block_size() == 0);
CHECK(file->get_size() == 0);
CHECK(cluster->get_blocks().size() == 0);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
}
SECTION("new dir inside a new ir") {
auto name_1 = bfs::path(L"п1");
auto name_2 = name_1 / bfs::path(L"п2");
auto dir_path = root_path / name_2;
bfs::create_directories(dir_path);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(cluster->get_blocks().size() == 0);
auto f_1 = files->by_name(narrow(name_1.generic_wstring()));
REQUIRE(f_1);
CHECK(f_1->is_locally_available());
CHECK(f_1->is_dir());
auto f_2 = files->by_name(narrow(name_2.generic_wstring()));
REQUIRE(f_2);
CHECK(f_2->is_locally_available());
CHECK(f_2->is_dir());
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
}
SECTION("empty file") {
CHECK(bfs::create_directories(root_path / "abc"));
auto file_path = root_path / "abc" / "empty.file";
write_file(file_path, "");
auto ingore_perms = GENERATE(0, 1);
auto folder_data = static_cast<model::folder_data_t *>(folder.get());
folder_data->access<test::to::ignore_permissions>() = (bool)ingore_perms;
builder->scan_start(folder->get_id()).apply(*sup);
auto file = files->by_name("abc/empty.file");
REQUIRE(file);
CHECK(file->is_locally_available());
CHECK(!file->is_link());
CHECK(file->is_file());
CHECK(file->get_block_size() == 0);
CHECK(file->get_size() == 0);
CHECK(blocks.size() == 0);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
#ifndef SYNCSPIRIT_WIN
if (ingore_perms) {
CHECK(file->has_no_permissions());
CHECK(file->get_permissions() == 0666);
} else {
CHECK(!file->has_no_permissions());
CHECK(file->get_permissions() == static_cast<uint32_t>(bfs::status(file_path).permissions()));
}
#else
CHECK(file->has_no_permissions());
CHECK(file->get_permissions() == 0666);
#endif
}
#ifndef SYNCSPIRIT_WIN
SECTION("new symlink") {
auto file_path = root_path / "symlink";
auto target = std::string_view("/some/where");
bfs::create_symlink(bfs::path(target), file_path, ec);
REQUIRE(!ec);
builder->scan_start(folder->get_id()).apply(*sup);
auto file = files->by_name("symlink");
REQUIRE(file);
CHECK(file->is_locally_available());
CHECK(!file->is_file());
CHECK(file->is_link());
CHECK(file->get_block_size() == 0);
CHECK(file->get_size() == 0);
CHECK(file->get_link_target() == target);
CHECK(file->has_no_permissions());
CHECK(blocks.size() == 0);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
}
#endif
SECTION("small non-emtpy file") {
CHECK(bfs::create_directories(root_path / L"папка"));
auto part_path = bfs::path(L"папка") / L"файл.bin";
auto file_path = root_path / part_path;
write_file(file_path, "12345");
builder->scan_start(folder->get_id()).apply(*sup);
auto file = files->by_name(narrow(part_path.generic_wstring()));
REQUIRE(file);
CHECK(file->is_locally_available());
CHECK(!file->is_link());
CHECK(file->is_file());
CHECK(file->get_block_size() == 5);
CHECK(file->get_size() == 5);
CHECK(blocks.size() == 1);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
}
SECTION("2 blocks file") {
CHECK(bfs::create_directories(root_path / L"папка"));
auto part_path = bfs::path(L"папка") / L"файл.bin";
auto file_path = root_path / part_path;
auto block_sz = fs::block_sizes[0];
auto b1 = std::string(block_sz, '0');
auto b2 = std::string(block_sz, '1');
write_file(file_path, b1 + b2);
builder->scan_start(folder->get_id()).apply(*sup);
auto file = files->by_name(narrow(part_path.generic_wstring()));
REQUIRE(file);
CHECK(file->is_locally_available());
CHECK(!file->is_link());
CHECK(file->is_file());
CHECK(file->get_block_size() == block_sz);
CHECK(file->get_size() == block_sz * 2);
CHECK(blocks.size() == 2);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
}
SECTION("2 blocks + 1 byte file") {
CHECK(bfs::create_directories(root_path / L"папка"));
auto part_path = bfs::path(L"папка") / L"файл.bin";
auto file_path = root_path / part_path;
auto block_sz = fs::block_sizes[0];
auto b1 = std::string(block_sz, '0');
auto b2 = std::string(block_sz, '1');
auto b3 = std::string(1, '2');
write_file(file_path, b1 + b2 + b3);
builder->scan_start(folder->get_id()).apply(*sup);
auto file = files->by_name(narrow(part_path.generic_wstring()));
REQUIRE(file);
CHECK(file->is_locally_available());
CHECK(!file->is_link());
CHECK(file->is_file());
CHECK(file->get_block_size() == block_sz);
CHECK(file->get_size() == block_sz * 2 + 1);
CHECK(blocks.size() == 3);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
}
}
}
};
F().run();
}
void test_create_dir() {
struct F : fixture_t {
void main() noexcept override {
auto folder_id = std::string(folder->get_id());
builder->remove_folder(*folder).apply(*sup);
bfs::remove_all(root_path);
REQUIRE(!bfs::exists(root_path));
SECTION("success") {
builder->upsert_folder(folder_id, root_path).apply(*sup);
CHECK(bfs::exists(root_path));
auto f = cluster->get_folders().by_id(folder_id);
REQUIRE(f);
REQUIRE(!f->is_suspended());
}
SECTION("fail & suspend") {
auto sub_path = root_path / "a" / "dir";
builder->upsert_folder(folder_id, sub_path).apply(*sup);
CHECK(!bfs::exists(sub_path));
auto f = cluster->get_folders().by_id(folder_id);
REQUIRE(f);
REQUIRE(f->is_suspended());
CHECK(f->get_suspend_reason());
}
}
};
F().run();
}
void test_no_changes() {
struct F : fixture_t {
std::uint32_t get_hash_limit() override { return 2; }
void main() noexcept override {
sys::error_code ec;
auto &blocks = cluster->get_blocks();
auto my_short_id = my_device->device_id().get_uint();
auto folder_id = folder->get_id();
auto v = proto::Vector();
auto &counter = proto::add_counters(v);
proto::set_id(counter, 1);
proto::set_value(counter, 1);
SECTION("single item") {
auto pr_file = proto::FileInfo{};
auto file_name = bfs::path(L"неизменное.bin");
proto::set_name(pr_file, file_name.string());
proto::set_sequence(pr_file, 4);
proto::set_version(pr_file, v);
SECTION("dir") {
auto dir = root_path / file_name;
bfs::create_directories(dir);
auto modified = to_unix(bfs::last_write_time(dir));
auto status = bfs::status(dir);
auto perms = static_cast<uint32_t>(status.permissions());
proto::set_type(pr_file, proto::FileInfoType::DIRECTORY);
proto::set_permissions(pr_file, perms);
proto::set_modified_s(pr_file, modified);
builder->local_update(folder_id, pr_file).apply(*sup);
REQUIRE(files->size() == 1);
auto file_1 = files->by_name(narrow(file_name.wstring()));
file_1->mark_local(false);
CHECK(!file_1->is_local());
builder->scan_start(folder_id).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
REQUIRE(files->size() == 1);
auto file_2 = files->by_name(narrow(file_name.wstring()));
CHECK(file_1.get() == file_2.get());
CHECK(file_1->is_local());
}
SECTION("regular file") {
auto path = root_path / file_name;
auto data = std::string("12345");
write_file(path, data);
auto modified = to_unix(bfs::last_write_time(path));
auto status = bfs::status(path);
auto perms = static_cast<uint32_t>(status.permissions());
proto::set_permissions(pr_file, perms);
proto::set_modified_s(pr_file, modified);
proto::set_size(pr_file, 5);
proto::set_block_size(pr_file, 5);
auto b = proto::BlockInfo();
proto::set_size(b, 5);
proto::set_hash(b, utils::sha256_digest(as_bytes(data)).value());
proto::add_blocks(pr_file, b);
builder->local_update(folder_id, pr_file).apply(*sup);
REQUIRE(files->size() == 1);
REQUIRE(blocks.size() == 1);
auto file_1 = files->by_name(narrow(file_name.wstring()));
file_1->mark_local(false);
CHECK(!file_1->is_local());
builder->scan_start(folder_id).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
REQUIRE(files->size() == 1);
auto file_2 = files->by_name(narrow(file_name.wstring()));
CHECK(file_1.get() == file_2.get());
CHECK(file_1->is_local());
REQUIRE(blocks.size() == 1);
#ifndef SYNCSPIRIT_WIN
SECTION("changes on permissions are ignored") {
proto::set_permissions(pr_file, perms - 1);
proto::set_no_permissions(pr_file, true);
builder->local_update(folder_id, pr_file).apply(*sup);
auto seq_1 = file_1->get_sequence();
builder->scan_start(folder_id).apply(*sup);
bfs::permissions(path, bfs::perms::none);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
auto seq_2 = file_1->get_sequence();
CHECK(seq_1 == seq_2);
}
#endif
}
#ifndef SYNCSPIRIT_WIN
SECTION("symlink") {
auto file_path = root_path / file_name;
auto target = bfs::path(L"/куда-то/where");
bfs::create_symlink(target, file_path);
auto status = bfs::symlink_status(file_path);
auto perms = static_cast<uint32_t>(status.permissions());
proto::set_symlink_target(pr_file, narrow(target.wstring()));
proto::set_type(pr_file, proto::FileInfoType::SYMLINK);
proto::set_permissions(pr_file, perms);
builder->local_update(folder_id, pr_file).apply(*sup);
REQUIRE(files->size() == 1);
auto file_1 = files->by_name(narrow(file_name.wstring()));
file_1->mark_local(false);
REQUIRE(file_1->is_link());
REQUIRE(file_1->get_link_target() == narrow(target.wstring()));
CHECK(!file_1->is_local());
builder->scan_start(folder_id).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
REQUIRE(files->size() == 1);
auto file_2 = files->by_name(narrow(file_name.wstring()));
CHECK(file_1.get() == file_2.get());
CHECK(file_1->is_local());
}
#endif
}
SECTION("dir & file") {
auto file_name_1 = bfs::path(L"b-dir");
auto dir = root_path / file_name_1;
bfs::create_directories(dir);
auto modified_1 = to_unix(bfs::last_write_time(dir));
auto perms_1 = static_cast<uint32_t>(bfs::status(dir).permissions());
auto pr_file_1 = proto::FileInfo{};
proto::set_name(pr_file_1, file_name_1.string());
proto::set_version(pr_file_1, v);
proto::set_type(pr_file_1, proto::FileInfoType::DIRECTORY);
proto::set_permissions(pr_file_1, perms_1);
proto::set_modified_s(pr_file_1, modified_1);
auto file_name_2 = bfs::path(L"a-file");
auto file = root_path / file_name_2;
write_file(file, "");
auto modified_2 = to_unix(bfs::last_write_time(file));
auto perms_2 = static_cast<uint32_t>(bfs::status(file).permissions());
auto pr_file_2 = proto::FileInfo{};
proto::set_name(pr_file_2, file_name_2.string());
proto::set_version(pr_file_2, v);
proto::set_type(pr_file_2, proto::FileInfoType::FILE);
proto::set_permissions(pr_file_2, perms_2);
proto::set_modified_s(pr_file_2, modified_2);
builder->local_update(folder_id, pr_file_1).local_update(folder_id, pr_file_2).apply(*sup);
REQUIRE(files->size() == 2);
auto sequence = folder_info->get_max_sequence();
builder->scan_start(folder_id).apply(*sup);
CHECK(folder_info->get_max_sequence() == sequence);
}
}
};
F().run();
}
void test_deleted() {
struct F : fixture_t {
using paths_t = std::vector<std::string>;
void on_diff(const model::diff::advance::local_update_t &diff) noexcept override {
auto file = folder_info->get_file_infos().by_uuid(diff.uuid);
auto name = file->get_name()->get_full_name();
paths.emplace_back(std::string(name));
}
void on_diff(const model::diff::local::file_availability_t &diff) noexcept override {
available.emplace_back(std::string(diff.name));
}
void main() noexcept override {
sys::error_code ec;
auto &blocks = cluster->get_blocks();
auto my_short_id = my_device->device_id().get_uint();
auto v = proto::Vector();
auto &counter = proto::add_counters(v);
proto::set_id(counter, my_short_id);
proto::set_value(counter, 1);
SECTION("single items") {
auto pr_file = proto::FileInfo{};
auto file_name = bfs::path(L"неизменное.bin");
proto::set_name(pr_file, file_name.string());
proto::set_sequence(pr_file, 4);
proto::set_version(pr_file, v);
SECTION("regular file") {
proto::set_type(pr_file, proto::FileInfoType::FILE);
builder->local_update(folder->get_id(), pr_file).apply(*sup);
}
#ifndef SYNCSPIRIT_WIN
SECTION("symlink") {
proto::set_symlink_target(pr_file, "does/not/matter");
proto::set_type(pr_file, proto::FileInfoType::SYMLINK);
builder->local_update(folder->get_id(), pr_file).apply(*sup);
}
#endif
SECTION("regular dir") {
proto::set_type(pr_file, proto::FileInfoType::DIRECTORY);
builder->local_update(folder->get_id(), pr_file).apply(*sup);
}
REQUIRE(files->size() == 1);
auto file_1 = files->by_name(narrow(file_name.wstring()));
file_1->mark_local(false);
CHECK(!file_1->is_local());
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
REQUIRE(files->size() == 1);
auto file_2 = files->by_name(narrow(file_name.wstring()));
CHECK(file_1.get() == file_2.get());
CHECK(file_1->is_local());
REQUIRE(file_1->is_deleted());
CHECK(file_info_t::as_type(file_1->get_type()) == proto::get_type(pr_file));
}
SECTION("deleted hierarchy of dirs") {
auto pr_file = proto::FileInfo{};
proto::set_type(pr_file, proto::FileInfoType::DIRECTORY);
proto::set_deleted(pr_file, false);
proto::set_sequence(pr_file, 4);
proto::set_version(pr_file, v);
auto file_name = bfs::path(L"имя");
for (auto &name : {"a", "a/bb", "a/cc", "a/bb/ddd"}) {
proto::set_name(pr_file, name);
builder->local_update(folder->get_id(), pr_file);
}
builder->apply(*sup);
REQUIRE(files->size() == 4);
for (auto f : *files) {
f->mark_local(false);
}
SECTION("whole hierarchy removal") {
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 4);
for (auto f : *files) {
CHECK(f->is_local());
CHECK(f->is_deleted());
}
}
SECTION("sub hierarchy removal") {
bfs::create_directories(root_path / "a" / "cc");
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 4);
auto dir_a = files->by_name("a");
REQUIRE(dir_a);
REQUIRE(dir_a->is_local());
REQUIRE(!dir_a->is_deleted());
auto dir_cc = files->by_name("a/cc");
REQUIRE(dir_cc);
REQUIRE(dir_cc->is_local());
REQUIRE(!dir_cc->is_deleted());
auto dir_bb = files->by_name("a/bb");
REQUIRE(dir_bb);
REQUIRE(dir_bb->is_local());
REQUIRE(dir_bb->is_deleted());
auto dir_dd = files->by_name("a/bb/ddd");
REQUIRE(dir_dd);
REQUIRE(dir_dd->is_local());
REQUIRE(dir_dd->is_deleted());
}
}
SECTION("order of deletion (1)") {
auto file_type = GENERATE(0, 1);
auto pr_file = proto::FileInfo{};
proto::set_type(pr_file, static_cast<proto::FileInfoType>(file_type));
proto::set_deleted(pr_file, false);
proto::set_sequence(pr_file, 4);
proto::set_version(pr_file, v);
for (auto &name : {"a", "b", "c", "d"}) {
proto::set_name(pr_file, name);
builder->local_update(folder->get_id(), pr_file);
}
builder->apply(*sup);
REQUIRE(files->size() == 4);
for (auto f : *files) {
f->mark_local(false);
}
paths = {};
builder->scan_start(folder->get_id()).apply(*sup);
auto expected = paths_t{
"a",
"b",
"c",
"d",
};
CHECK(paths == expected);
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(paths == expected);
}
SECTION("order of deletion (2)") {
auto file_type = GENERATE(0, 1);
auto pr_file = proto::FileInfo{};
proto::set_type(pr_file, proto::FileInfoType::DIRECTORY);
proto::set_deleted(pr_file, false);
proto::set_sequence(pr_file, 4);
proto::set_version(pr_file, v);
for (auto &name : {"a", "a/1", "a/2", "a/3", "b", "b/1", "b/2", "b/3", "a/2/xx", "a/2/yy"}) {
proto::set_name(pr_file, name);
builder->local_update(folder->get_id(), pr_file);
}
builder->apply(*sup);
REQUIRE(files->size() == 10);
for (auto f : *files) {
f->mark_local(false);
}
paths = {};
builder->scan_start(folder->get_id()).apply(*sup);
// clang-format off
auto expected_rm = paths_t{
"a/1",
"a/2/xx",
"a/2/yy",
"a/2",
"a/3",
"a",
"b/1",
"b/2",
"b/3",
"b",
};
// clang-format on
auto expected_avail = paths_t{
"a", "a/1", "a/2", "a/2/xx", "a/2/yy", "a/3", "b", "b/1", "b/2", "b/3",
};
// clang-format on
CHECK(paths == expected_rm);
available = {};
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(paths == expected_rm);
CHECK(available == expected_avail);
}
}
paths_t paths;
paths_t available;
};
F().run();
}
void test_changed() {
struct F : fixture_t {
void main() noexcept override {
sys::error_code ec;
auto &blocks = cluster->get_blocks();
auto my_short_id = my_device->device_id().get_uint();
auto v = proto::Vector();
auto &counter = proto::add_counters(v);
proto::set_id(counter, my_short_id);
proto::set_value(counter, 1);
SECTION("change all content of a small file") {
auto data_1 = as_owned_bytes("12345");
auto data_2 = as_owned_bytes("67890");
auto data_x = as_owned_bytes("5432109876");
auto hash_1 = utils::sha256_digest(data_1).value();
auto hash_2 = utils::sha256_digest(data_2).value();
auto hash_4 = utils::sha256_digest(data_x).value();
auto pr_file = proto::FileInfo{};
auto file_name = bfs::path(L"файлик.bin");
auto file_path = root_path / file_name;
proto::set_name(pr_file, file_name.string());
proto::set_sequence(pr_file, 4);
proto::set_version(pr_file, v);
auto b_1 = proto::BlockInfo();
proto::set_hash(b_1, hash_1);
proto::set_size(b_1, data_1.size());
auto b_2 = proto::BlockInfo();
proto::set_hash(b_2, hash_2);
proto::set_offset(b_2, data_1.size());
proto::set_size(b_2, data_2.size());
proto::add_blocks(pr_file, b_1);
proto::add_blocks(pr_file, b_2);
proto::set_size(pr_file, data_1.size() + data_2.size());
builder->local_update(folder->get_id(), pr_file).apply(*sup);
REQUIRE(files->size() == 1);
REQUIRE(blocks.size() == 2);
write_file(file_path, "5432109876");
auto file_1 = files->by_name(narrow(file_name.wstring()));
file_1->mark_local(false);
auto seq_1 = file_1->get_sequence();
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 1);
CHECK(blocks.size() == 1);
auto file_2 = files->by_name(narrow(file_name.wstring()));
CHECK(file_2->is_local());
REQUIRE(file_2->iterate_blocks().get_total() == 1);
CHECK(file_2->iterate_blocks().next()->get_hash() == hash_4);
auto seq_2 = file_2->get_sequence();
CHECK(seq_2 > seq_1);
}
SECTION("file meta changed") {
auto data_1_str = std::string("12345");
auto data_1 = as_owned_bytes(data_1_str);
auto hash_1 = utils::sha256_digest(data_1).value();
auto pr_file = proto::FileInfo{};
auto file_name = bfs::path(L"файлик.bin");
auto file_path = root_path / file_name;
proto::set_name(pr_file, file_name.string());
proto::set_sequence(pr_file, 4);
proto::set_version(pr_file, v);
auto b_1 = proto::BlockInfo();
proto::set_hash(b_1, hash_1);
proto::set_size(b_1, data_1.size());
proto::add_blocks(pr_file, b_1);
proto::set_size(pr_file, data_1.size());
write_file(file_path, data_1_str);
auto status = bfs::status(file_path);
auto modified = to_unix(bfs::last_write_time(file_path));
auto perms = static_cast<uint32_t>(0444);
bool perms_changed = false;
SECTION("modification time changed") {
proto::set_permissions(pr_file, perms);
proto::set_modified_s(pr_file, modified - 1);
}
SECTION("permissions changed") {
proto::set_permissions(pr_file, perms - 1);
proto::set_modified_s(pr_file, modified);
perms_changed = true;
}
builder->local_update(folder->get_id(), pr_file).apply(*sup);
REQUIRE(files->size() == 1);
REQUIRE(blocks.size() == 1);
auto file_1 = files->by_name(narrow(file_name.wstring()));
file_1->mark_local(false);
auto seq_1 = file_1->get_sequence();
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 1);
auto file_2 = files->by_name(narrow(file_name.wstring()));
CHECK(file_2->is_local());
REQUIRE(file_2->iterate_blocks().get_total() == 1);
CHECK(file_2->iterate_blocks().next()->get_hash() == hash_1);
auto seq_2 = file_2->get_sequence();
if (!perms_changed || utils::platform_t::permissions_supported(file_path)) {
CHECK(seq_2 > seq_1);
} else {
CHECK(seq_2 == seq_1);
}
}
SECTION("dir meta changed") {
auto pr_file = proto::FileInfo{};
auto file_name = bfs::path(L"папка");
auto file_path = root_path / file_name;
bfs::create_directories(file_path);
auto status = bfs::status(file_path);
auto modified = to_unix(bfs::last_write_time(file_path));
auto perms = static_cast<uint32_t>(status.permissions());
bool perms_changed = false;
proto::set_name(pr_file, file_name.string());
proto::set_sequence(pr_file, 4);
proto::set_version(pr_file, v);
proto::set_type(pr_file, proto::FileInfoType::DIRECTORY);
SECTION("modification time changed") {
proto::set_permissions(pr_file, perms);
proto::set_modified_s(pr_file, modified - 1);
}
SECTION("permissions changed") {
proto::set_permissions(pr_file, perms - 1);
proto::set_modified_s(pr_file, modified);
perms_changed = true;
}
builder->local_update(folder->get_id(), pr_file).apply(*sup);
REQUIRE(files->size() == 1);
auto file_1 = files->by_name(narrow(file_name.wstring()));
file_1->mark_local(false);
auto seq_1 = file_1->get_sequence();
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 1);
auto file_2 = files->by_name(narrow(file_name.wstring()));
CHECK(file_2->is_local());
auto seq_2 = file_2->get_sequence();
if (perms_changed && utils::platform_t::permissions_supported(file_path)) {
CHECK(seq_2 > seq_1);
} else {
CHECK(seq_2 == seq_1);
}
}
#ifndef SYNCSPIRIT_WIN
SECTION("symlink target changed") {
auto file_path = root_path / "symlink";
auto target_1 = std::string_view("/some/where");
bfs::create_symlink(bfs::path(target_1), file_path, ec);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(files->size() == 1);
auto file_1 = files->by_name(file_path.filename().string());
REQUIRE(file_1);
auto seq_1 = file_1->get_sequence();
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
bfs::remove_all(file_path);
auto target_2 = std::string_view("/some/where/2");
bfs::create_symlink(bfs::path(target_2), file_path, ec);
builder->scan_start(folder->get_id()).apply(*sup);
auto file_2 = files->by_name(file_path.filename().string());
REQUIRE(file_2);
auto seq_2 = file_2->get_sequence();
CHECK(seq_2 > seq_1);
}
#endif
SECTION("append new block") {
auto block_sz = fs::block_sizes[0];
auto b1 = std::string(block_sz, '0');
auto b2 = std::string(block_sz, '1');
auto b3 = std::string(block_sz, '2');
auto data_1 = as_owned_bytes(b1);
auto data_2 = as_owned_bytes(b2);
auto data_3 = as_owned_bytes(b3);
auto hash_1 = utils::sha256_digest(data_1).value();
auto hash_2 = utils::sha256_digest(data_2).value();
auto hash_3 = utils::sha256_digest(data_3).value();
auto pr_file = proto::FileInfo{};
auto file_name = bfs::path(L"файлик.bin");
auto file_path = root_path / file_name;
proto::set_name(pr_file, file_name.string());
proto::set_sequence(pr_file, 4);
proto::set_version(pr_file, v);
auto b_1 = proto::BlockInfo();
proto::set_hash(b_1, hash_1);
proto::set_size(b_1, data_1.size());
auto b_2 = proto::BlockInfo();
proto::set_hash(b_2, hash_2);
proto::set_offset(b_2, data_1.size());
proto::set_size(b_2, data_2.size());
proto::add_blocks(pr_file, b_1);
proto::add_blocks(pr_file, b_2);
proto::set_size(pr_file, data_1.size() + data_2.size());
builder->local_update(folder->get_id(), pr_file).apply(*sup);
REQUIRE(files->size() == 1);
REQUIRE(blocks.size() == 2);
write_file(file_path, b1 + b2 + b3);
auto file_1 = files->by_name(narrow(file_name.wstring()));
file_1->mark_local(false);
auto seq_1 = file_1->get_sequence();
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 1);
CHECK(blocks.size() == 3);
auto file_2 = files->by_name(narrow(file_name.wstring()));
CHECK(file_2->is_local());
REQUIRE(file_2->iterate_blocks().get_total() == 3);
REQUIRE(file_2->iterate_blocks(2).next()->get_hash() == hash_3);
REQUIRE(file_2->get_size() == block_sz * 3);
auto seq_2 = file_2->get_sequence();
CHECK(seq_2 > seq_1);
}
}
};
F().run();
}
void test_type_change() {
struct F : fixture_t {
void main() noexcept override {
auto my_short_id = my_device->device_id().get_uint();
auto pr_file = proto::FileInfo{};
auto file_name = bfs::path(L"файлик.bin");
proto::set_name(pr_file, file_name.string());
proto::set_sequence(pr_file, 4);
auto &v = proto::get_version(pr_file);
auto &counter = proto::add_counters(v);
proto::set_id(counter, my_short_id);
proto::set_value(counter, 1);
auto file_path = root_path / file_name;
SECTION("has been dir") {
proto::set_type(pr_file, proto::FileInfoType::DIRECTORY);
builder->local_update(folder->get_id(), pr_file).apply(*sup);
auto file_1 = files->by_name(narrow(file_name.wstring()));
REQUIRE(file_1->is_dir());
auto seq_1 = file_1->get_sequence();
SECTION(" -> regular file") {
write_file(file_path, "");
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
auto file_2 = files->by_name(narrow(file_name.wstring()));
REQUIRE(file_2->is_file());
auto seq_2 = file_2->get_sequence();
CHECK(seq_2 > seq_1);
}
#ifndef SYNCSPIRIT_WIN
SECTION(" -> symlink") {
bfs::create_symlink(bfs::path("/some/where"), file_path);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
auto file_2 = files->by_name(narrow(file_name.wstring()));
REQUIRE(file_2->is_link());
auto seq_2 = file_2->get_sequence();
CHECK(seq_2 > seq_1);
}
#endif
}
SECTION("has been regular file") {
proto::set_type(pr_file, proto::FileInfoType::FILE);
builder->local_update(folder->get_id(), pr_file).apply(*sup);
auto file_1 = files->by_name(narrow(file_name.wstring()));
REQUIRE(file_1->is_file());
auto seq_1 = file_1->get_sequence();
SECTION(" -> dir") {
bfs::create_directories(file_path);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
auto file_2 = files->by_name(narrow(file_name.wstring()));
REQUIRE(file_2->is_dir());
auto seq_2 = file_2->get_sequence();
CHECK(seq_2 > seq_1);
}
#ifndef SYNCSPIRIT_WIN
SECTION(" -> symlink") {
bfs::create_symlink(bfs::path("/some/where"), file_path);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
auto file_2 = files->by_name(narrow(file_name.wstring()));
REQUIRE(file_2->is_link());
auto seq_2 = file_2->get_sequence();
CHECK(seq_2 > seq_1);
}
#endif
}
#ifndef SYNCSPIRIT_WIN
SECTION("has been symlink") {
proto::set_type(pr_file, proto::FileInfoType::SYMLINK);
builder->local_update(folder->get_id(), pr_file).apply(*sup);
auto file_1 = files->by_name(narrow(file_name.wstring()));
REQUIRE(file_1->is_link());
auto seq_1 = file_1->get_sequence();
SECTION(" -> dir") {
bfs::create_directories(file_path);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
auto file_2 = files->by_name(narrow(file_name.wstring()));
REQUIRE(file_2->is_dir());
auto seq_2 = file_2->get_sequence();
CHECK(seq_2 > seq_1);
}
SECTION(" -> regular file") {
write_file(file_path, "");
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
auto file_2 = files->by_name(narrow(file_name.wstring()));
REQUIRE(file_2->is_file());
auto seq_2 = file_2->get_sequence();
CHECK(seq_2 > seq_1);
}
}
#endif
}
};
F().run();
}
void test_resurrection() {
struct F : fixture_t {
void main() noexcept override {
bfs::create_directories(root_path / L"a/b/c");
write_file(root_path / "a/b/c/file.bin", "12345");
builder->scan_start(folder->get_id()).apply(*sup);
auto &files = folder_info->get_file_infos();
CHECK(files.size() == 4);
auto dir_a = files.by_name("a");
auto dir_b = files.by_name("a/b");
auto dir_c = files.by_name("a/b/c");
auto file = files.by_name("a/b/c/file.bin");
REQUIRE(dir_a);
REQUIRE(dir_b);
REQUIRE(dir_c);
REQUIRE(file);
REQUIRE(file->get_size() == 5);
CHECK(!dir_a->is_deleted());
CHECK(!dir_b->is_deleted());
CHECK(!dir_c->is_deleted());
CHECK(!file->is_deleted());
bfs::remove_all(root_path / L"a");
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(dir_a->is_deleted());
CHECK(dir_b->is_deleted());
CHECK(dir_c->is_deleted());
CHECK(file->is_deleted());
bfs::create_directories(root_path / L"a/b/c");
write_file(root_path / "a/b/c/file.bin", "12345");
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!dir_a->is_deleted());
CHECK(!dir_b->is_deleted());
CHECK(!dir_c->is_deleted());
CHECK(!file->is_deleted());
REQUIRE(file->get_size() == 5);
}
};
F().run();
}
void test_partial_scan() {
struct F : fixture_t {
void main() noexcept override {
auto my_short_id = my_device->device_id().get_uint();
auto dir_1 = root_path / L"а/б/в";
auto dir_2 = root_path / L"x/y/z";
auto file_1 = dir_1 / L"файл-1.bin";
auto file_2 = dir_2 / L"файл-2.bin";
bfs::create_directories(dir_1);
bfs::create_directories(dir_2);
write_file(file_1, "12345");
write_file(file_2, "67890");
builder->scan_start(folder->get_id()).apply(*sup);
auto &files = folder_info->get_file_infos();
REQUIRE(files.size() == 6 + 2);
auto f_1 = files.by_name(narrow(L"а/б/в/файл-1.bin"));
auto f_2 = files.by_name(narrow(L"x/y/z/файл-2.bin"));
REQUIRE(f_1);
REQUIRE(f_2);
auto seq_1 = f_1->get_sequence();
auto seq_2 = f_2->get_sequence();
write_file(file_1, "1234567890");
write_file(file_2, "6789012345");
SECTION("existing dir") {
auto subdir = GENERATE(L"а", L"а/б", L"а/б/в");
builder->scan_start(folder->get_id(), narrow(subdir)).apply(*sup);
CHECK(subdir != std::wstring_view());
REQUIRE(files.size() == 6 + 2);
CHECK(f_1->get_sequence() != seq_1);
CHECK(f_2->get_sequence() == seq_2);
}
SECTION("non-existing dir") {
builder->scan_start(folder->get_id()).apply(*sup);
auto subdir = narrow(GENERATE(L"а", L"а/б", L"а/б/в"));
bfs::remove_all(root_path / subdir);
INFO("subdir: " << subdir);
CHECK(subdir != std::string_view{});
builder->scan_start(folder->get_id(), narrow(L"а/б/в")).apply(*sup);
auto dir = files.by_name(narrow(L"а/б/в"));
REQUIRE(dir);
auto aug = folder->get_augmentation().get();
auto folder_entity = static_cast<presentation::folder_entity_t *>(aug);
auto &a_entity = *folder_entity->get_children().begin();
auto &b_entity = *a_entity->get_children().begin();
auto &c_entity = *b_entity->get_children().begin();
auto presence = c_entity->get_presence(my_device.get());
auto &entity_stats = c_entity->get_stats();
auto &presence_stats = presence->get_stats();
CHECK(entity_stats.entities == 2);
CHECK(entity_stats.entities == presence_stats.local_entries);
CHECK(!folder->is_suspended());
}
SECTION("resurrection") {
bfs::remove_all(root_path / L"а");
auto dir_a = files.by_name(narrow(L"а"));
auto dir_x = files.by_name("x");
auto dir_b = files.by_name(narrow(L"а/б"));
auto dir_c = files.by_name(narrow(L"а/б/в"));
builder->scan_start(folder->get_id(), narrow(L"а/б")).apply(*sup);
REQUIRE(dir_a->is_deleted());
REQUIRE(dir_b->is_deleted());
REQUIRE(dir_c->is_deleted());
CHECK(!dir_x->is_deleted());
bfs::create_directories(root_path / L"а/б/в");
builder->scan_start(folder->get_id(), narrow(L"а/б")).apply(*sup);
CHECK(!dir_a->is_deleted());
CHECK(!dir_b->is_deleted());
CHECK(!dir_c->is_deleted());
CHECK(!dir_x->is_deleted());
}
}
};
F().run();
}
void test_scan_errors() {
struct F : fixture_t {
F() {
processor = [&](fs::fs_slave_t *slave) {
slave->ec = {};
sup->log->info("executing foreign task: {}", execute_slave(*slave));
};
}
std::uint32_t get_hash_limit() override { return 100; }
void execute(fs::message::foreign_executor_t &req) noexcept override {
++exec_attempts;
if (exec_pool) {
sup->log->info("processing foreign task (left = {})", exec_pool);
auto slave = static_cast<fs::fs_slave_t *>(req.payload.get());
processor(slave);
--exec_pool;
} else {
sup->log->info("ignoring foreign task execution");
}
}
void main() noexcept override {
SECTION("root dir errors") {
SECTION("missing root dir") {
auto dir_path = root_path / "some-dir";
folder->set_path(dir_path);
}
SECTION("no permissings to read outer dir") {
auto dir_path = root_path / "some-dir";
folder->set_path(dir_path);
bfs::permissions(root_path, bfs::perms::none);
}
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(folder->is_suspended());
REQUIRE(folder->get_suspend_reason().message() != "");
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
bfs::permissions(root_path, bfs::perms::all);
CHECK(exec_attempts == 1);
}
SECTION("missing start dir (in model)") {
builder->scan_start(folder->get_id(), "non-existing-dir").apply(*sup);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(exec_attempts == 0);
}
#ifndef SYNCSPIRIT_WIN
SECTION("non-root errors (non-win32)") {
auto dir_path = root_path / "d1" / "d2";
auto d1_path = dir_path.parent_path();
bfs::create_directories(dir_path);
auto status = bfs::status(d1_path);
auto perms = status.permissions();
bfs::permissions(d1_path, perms, bfs::perm_options::remove);
builder->scan_start(folder->get_id()).apply(*sup);
auto ec = sys::error_code{};
bfs::create_directories(d1_path / L"авось", ec);
bfs::permissions(d1_path, perms, bfs::perm_options::add);
if (ec) {
REQUIRE(!folder->is_suspended());
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 0);
} else {
INFO("Skipping due to unability to prohibit directories creation");
}
}
SECTION("non-sync'able entity (named fifo file)") {
auto fifo_path = root_path / "fifo";
auto fifo_str = narrow(fifo_path.wstring());
REQUIRE(mknod(fifo_str.c_str(), S_IFIFO | 0666, 0) == 0);
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 0);
}
#endif
SECTION("generic task error") {
exec_pool = 1;
auto dir_path = root_path / "d1" / "d2";
auto d1_path = dir_path.parent_path();
bfs::create_directories(dir_path);
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 1);
}
SECTION("scan dir errors") {
int mocked = 0;
auto generator_type = GENERATE(0, 1);
auto do_mock = [&](bfs::path dir_path) {
processor = [&, dir_path = dir_path](fs::fs_slave_t *slave) {
slave->ec = {};
bool do_exec = true;
if (!slave->tasks_in.empty()) {
auto task = &slave->tasks_in.front();
auto scan_task = std::get_if<fs::task::scan_dir_t>(task);
if (scan_task) {
if (scan_task->path == dir_path) {
++mocked;
do_exec = false;
auto ec = r::make_error_code(r::error_code_t::cancelled);
sup->log->info("mocking result {}", generator_type);
if (generator_type == 0) {
scan_task->ec = {};
auto info = fs::task::scan_dir_t::child_info_t();
info.path = dir_path / "xx";
info.ec = ec;
scan_task->child_infos.emplace_back(std::move(info));
} else {
scan_task->ec = ec;
}
slave->tasks_out.emplace_back(std::move(*task));
slave->tasks_in.pop_front();
}
}
}
if (do_exec) {
sup->log->info("executing foreign task: {}", execute_slave(*slave));
}
};
};
auto my_short_id = my_device->device_id().get_uint();
auto dir_path = root_path / "d1";
auto d1_path = dir_path.parent_path();
bfs::create_directories(dir_path);
auto pr_dir = proto::FileInfo{};
proto::set_name(pr_dir, "d1");
proto::set_sequence(pr_dir, 4);
auto &v = proto::get_version(pr_dir);
auto &counter = proto::add_counters(v);
proto::set_id(counter, my_short_id);
proto::set_value(counter, 1);
builder->local_update(folder->get_id(), pr_dir).apply(*sup);
CHECK(files->size() == 1);
auto dir_file = *files->begin();
SECTION("new file") {
do_mock(dir_path);
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(files->size() == 1);
}
SECTION("existing file") {
auto pr_file = proto::FileInfo{};
proto::set_name(pr_file, "d1/xx");
proto::set_sequence(pr_file, 5);
auto &v = proto::get_version(pr_file);
auto &counter = proto::add_counters(v);
proto::set_id(counter, my_short_id);
proto::set_value(counter, 1);
builder->local_update(folder->get_id(), pr_file).apply(*sup);
do_mock(dir_path);
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(files->size() == 2);
auto file = files->by_name("d1/xx");
CHECK(file->is_locally_available());
}
CHECK(dir_file->is_locally_available());
CHECK(mocked == 1);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
}
}
std::uint32_t exec_pool = 5;
std::uint32_t exec_attempts = 0;
task_processor_t processor;
};
F().run();
}
void test_read_errors() {
struct F : fixture_t {
F() : fixture_t{false} {
processor = [&](fs::fs_slave_t *slave) {
slave->ec = {};
sup->log->info("executing foreign task: {}", execute_slave(*slave));
};
}
std::uint32_t get_hash_limit() override { return hash_limit; }
void execute(fs::message::foreign_executor_t &req) noexcept override {
if (exec_pool) {
sup->log->info("processing foreign task (left = {})", exec_pool);
auto slave = static_cast<fs::fs_slave_t *>(req.payload.get());
processor(slave);
--exec_pool;
} else {
sup->log->info("ignoring foreign task execution");
}
}
void main() noexcept override {
#ifndef SYNCSPIRIT_WIN
SECTION("small unknown/new file") {
launch_target();
auto file_path = root_path / "file.bin";
write_file(file_path, "12345");
bfs::permissions(file_path, bfs::perms::all, bfs::perm_options::remove);
if (read_file(file_path) == "") {
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 0);
}
}
SECTION("small known file") {
launch_target();
auto file_path = root_path / "file.bin";
write_file(file_path, "12345");
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
REQUIRE(files->size() == 1);
auto file_1 = *files->begin();
CHECK(!file_1->is_unreachable());
CHECK(file_1->is_locally_available());
auto max_seq = folder_info->get_max_sequence();
bfs::permissions(file_path, bfs::perms::all, bfs::perm_options::remove);
if (read_file(file_path) == "") {
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
REQUIRE(files->size() == 1);
CHECK(file_1->is_unreachable());
CHECK(!file_1->is_locally_available());
CHECK(folder_info->get_max_sequence() == max_seq);
}
}
#endif
SECTION("large unknown file") {
auto file_path = root_path / "file.bin";
auto block_sz = fs::block_sizes[0];
auto count = std::uint32_t{5};
auto b = std::string(block_sz * count, 'x');
write_file(file_path, b);
auto do_mock = [&](std::int32_t block_index) {
processor = [&, count, block_index](fs::fs_slave_t *slave) {
slave->ec = {};
bool do_exec = true;
if (!slave->tasks_in.empty()) {
auto task = &slave->tasks_in.front();
auto iterator = std::get_if<fs::task::segment_iterator_t>(task);
if (iterator) {
if (iterator->block_index == block_index) {
do_exec = false;
auto ec = r::make_error_code(r::error_code_t::cancelled);
iterator->ec = ec;
sup->log->info("mocking result {} block", block_index);
slave->tasks_out.emplace_back(std::move(*task));
slave->tasks_in.pop_front();
}
}
}
if (do_exec) {
sup->log->info("executing foreign task: {}", execute_slave(*slave));
}
};
};
using pair_t = std::tuple<std::int32_t, std::int32_t>;
// clang-format off
auto pair = GENERATE(table<std::int32_t, std::int32_t>({
pair_t{0, 1},
pair_t{1, 1},
pair_t{2, 1},
pair_t{3, 1},
pair_t{4, 1},
pair_t{2, 2},
}));
// clang-format on
std::int32_t block_index;
std::tie(block_index, hash_limit) = pair;
do_mock(block_index);
launch_target();
INFO("block index = " << block_index << ", hash limit = " << hash_limit);
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 0);
CHECK(hasher->digested_blocks <= block_index + hash_limit);
}
SECTION("cannot read existing dir") {
#ifndef SYNCSPIRIT_WIN
launch_target();
auto dir_path = root_path / L"папка";
auto subdir_path = dir_path / L"подпапка";
auto file_path = subdir_path / "файл.bin";
bfs::create_directories(subdir_path);
write_file(file_path, "12345");
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 3);
auto file_1 = files->by_name(narrow(L"папка"));
auto file_2 = files->by_name(narrow(L"папка/подпапка"));
auto file_3 = files->by_name(narrow(L"папка/подпапка/файл.bin"));
REQUIRE(file_1);
REQUIRE(file_2);
REQUIRE(file_3);
auto max_seq = folder_info->get_max_sequence();
auto perms = bfs::status(dir_path).permissions();
bfs::permissions(dir_path, bfs::perms::all, bfs::perm_options::remove);
auto ec = sys::error_code{};
bfs::create_directories(dir_path / L"авось", ec);
if (ec) {
builder->scan_start(folder->get_id()).apply(*sup);
bfs::permissions(dir_path, perms, bfs::perm_options::replace);
CHECK(!folder->is_scanning());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 3);
CHECK(folder_info->get_max_sequence() == max_seq);
CHECK(file_1->is_unreachable());
CHECK(file_2->is_unreachable());
CHECK(file_3->is_unreachable());
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 3);
CHECK(folder_info->get_max_sequence() == max_seq);
CHECK(!file_1->is_unreachable());
CHECK(!file_2->is_unreachable());
CHECK(!file_3->is_unreachable());
}
#endif
}
}
std::uint32_t exec_pool = 10;
task_processor_t processor;
std::uint32_t hash_limit = 10;
};
F().run();
}
void test_leaks() {
struct F : fixture_t {
void launch_hasher() noexcept override {
hasher = sup->create_actor<managed_hasher_t>().index(1).auto_reply(false).timeout(timeout).finish().get();
}
void main() noexcept override {
write_file(root_path / "file-1.bin", "12345");
write_file(root_path / "file-2.bin", "12345");
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(folder->is_scanning());
target->do_shutdown();
sup->do_process();
CHECK(files->size() == 0);
hasher->process_requests();
sup->do_process();
}
};
F().run();
}
void test_hashing_fail() {
struct F : fixture_t {
std::uint32_t get_hash_limit() override { return 2; }
void launch_hasher() noexcept override {
hasher = sup->create_actor<managed_hasher_t>().index(1).subscribe(false).timeout(timeout).finish().get();
}
void main() noexcept override {
auto block_sz = fs::block_sizes[0];
auto b = std::string(block_sz * 5, 'x');
write_file(root_path / "file.bin", b);
builder->scan_start(folder->get_id()).apply(*sup);
sup->do_process();
CHECK(!folder->get_scan_start().is_special());
CHECK(!folder->get_scan_finish().is_special());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
CHECK(files->size() == 0);
}
};
F().run();
}
void test_incomplete() {
struct F : fixture_t {
void main() noexcept override {
using clock_t = std::chrono::system_clock;
auto sha256 = peer_device->device_id().get_sha256();
auto block_sz = fs::block_sizes[0];
auto path = root_path / L"файл.syncspirit-tmp";
auto pr_file = proto::FileInfo{};
auto file_name = narrow(L"файл");
proto::set_name(pr_file, file_name);
proto::set_sequence(pr_file, 4);
auto &v = proto::get_version(pr_file);
auto &counter = proto::add_counters(v);
proto::set_id(counter, 55);
proto::set_value(counter, 2);
proto::set_modified_s(pr_file, 12345);
auto data_1 = as_owned_bytes("12345");
auto data_2 = as_owned_bytes("67890");
auto hash_1 = utils::sha256_digest(data_1).value();
auto hash_2 = utils::sha256_digest(data_2).value();
auto b_1 = proto::BlockInfo();
proto::set_hash(b_1, hash_1);
proto::set_size(b_1, data_1.size());
auto b_2 = proto::BlockInfo();
proto::set_hash(b_2, hash_2);
proto::set_offset(b_2, data_1.size());
proto::set_size(b_2, data_2.size());
auto m_time = clock_t::to_time_t(clock_t::now()) - (constants::tmp_min_age * 2);
SECTION("no in model => remove") {
write_file(path, "");
last_write_time(path, fs::from_unix(m_time));
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(files->size() == 0);
CHECK(!bfs::exists(path));
}
SECTION("exists only in my model => remove") {
write_file(path, "");
last_write_time(path, fs::from_unix(m_time));
builder->local_update(folder->get_id(), pr_file)
.apply(*sup)
.then()
.scan_start(folder->get_id())
.apply(*sup);
CHECK(files->size() == 1);
CHECK(!bfs::exists(path));
}
SECTION("found in peer model, remove") {
auto should_not_exist = true;
SECTION("size mise mismatch") {
proto::add_blocks(pr_file, b_1);
proto::set_size(pr_file, data_1.size());
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
builder->scan_start(folder->get_id()).apply(*sup);
write_file(path, "");
last_write_time(path, fs::from_unix(m_time));
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(files->size() == 0);
}
#ifndef SYNCSPIRIT_WIN
SECTION("cannot read tmp file") {
write_file(path, "12345");
last_write_time(path, fs::from_unix(m_time));
auto status = bfs::status(path);
auto perms = static_cast<uint32_t>(status.permissions());
proto::add_blocks(pr_file, b_1);
proto::set_size(pr_file, data_1.size());
proto::set_permissions(pr_file, perms);
proto::set_modified_s(pr_file, to_unix(bfs::last_write_time(path)));
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
bfs::permissions(path, bfs::perms::all, bfs::perm_options::remove);
if (read_file(path) == "") {
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(files->size() == 0);
} else {
builder->scan_start(folder->get_id()).apply(*sup);
should_not_exist = false;
}
}
#endif
SECTION("local version is better than remote (local file does exists)") {
write_file(path, "1234");
last_write_time(path, fs::from_unix(m_time));
auto p = root_path / widen(file_name);
write_file(p, "12345");
last_write_time(path, fs::from_unix(m_time));
auto status = bfs::status(p);
auto perms = static_cast<uint32_t>(status.permissions());
proto::add_blocks(pr_file, b_1);
proto::set_size(pr_file, data_1.size());
proto::set_permissions(pr_file, perms);
proto::set_modified_s(pr_file, to_unix(bfs::last_write_time(p)));
builder->local_update(folder->get_id(), pr_file).apply(*sup);
REQUIRE(files->size() == 1);
auto seq_1 = folder_info->get_max_sequence();
proto::set_value(counter, 1);
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
builder->scan_start(folder->get_id()).apply(*sup);
auto seq_2 = folder_info->get_max_sequence();
REQUIRE(seq_1 == seq_2);
}
if (should_not_exist) {
CHECK(!bfs::exists(path));
}
}
SECTION("2 blocks peer file") {
proto::add_blocks(pr_file, b_1);
proto::add_blocks(pr_file, b_2);
proto::set_size(pr_file, data_1.size() + data_2.size());
auto model_path = root_path / widen(file_name);
SECTION("all blocks match => rename & add into model") {
write_file(path, "1234567890");
last_write_time(path, fs::from_unix(m_time));
auto status = bfs::status(path);
auto perms = static_cast<uint32_t>(status.permissions());
proto::set_permissions(pr_file, perms);
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
auto max_seq = folder_info->get_max_sequence();
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!bfs::exists(path));
CHECK(bfs::exists(model_path));
CHECK(read_file(model_path) == "1234567890");
CHECK(files->size() == 1);
auto f = files->by_name(file_name);
REQUIRE(f);
CHECK(f->get_size() == 10);
REQUIRE(f->iterate_blocks().get_total() == 2);
CHECK(f->iterate_blocks(0).next()->get_hash() == hash_1);
CHECK(f->iterate_blocks(1).next()->get_hash() == hash_2);
CHECK(folder_info->get_max_sequence() == max_seq + 1);
builder->scan_start(folder->get_id()).apply(*sup);
auto f2 = files->by_name(file_name);
CHECK(f2.get() == f.get());
REQUIRE(f2->iterate_blocks().get_total() == 2);
CHECK(folder_info->get_max_sequence() == max_seq + 1);
}
SECTION("1st block match") {
write_file(path, "1234500000");
last_write_time(path, fs::from_unix(m_time));
auto status = bfs::status(path);
auto perms = static_cast<uint32_t>(status.permissions());
proto::set_permissions(pr_file, perms);
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(bfs::exists(path));
CHECK(!bfs::exists(model_path));
CHECK(files->size() == 0);
auto peer_file = folder_info_peer->get_file_infos().by_name(file_name);
CHECK(peer_file->iterate_blocks(0).current().first->local_file());
CHECK(!peer_file->iterate_blocks(1).current().first->local_file());
}
SECTION("2nd block match") {
write_file(path, "0000067890");
last_write_time(path, fs::from_unix(m_time));
auto status = bfs::status(path);
auto perms = static_cast<uint32_t>(status.permissions());
proto::set_permissions(pr_file, perms);
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(bfs::exists(path));
CHECK(!bfs::exists(model_path));
CHECK(files->size() == 0);
auto peer_file = folder_info_peer->get_file_infos().by_name(file_name);
CHECK(!peer_file->iterate_blocks(0).current().first->local_file());
CHECK(peer_file->iterate_blocks(1).current().first->local_file());
}
SECTION("no block match") {
write_file(path, "0000000000");
last_write_time(path, fs::from_unix(m_time));
auto status = bfs::status(path);
auto perms = static_cast<uint32_t>(status.permissions());
proto::set_permissions(pr_file, perms);
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!bfs::exists(path));
CHECK(!bfs::exists(model_path));
CHECK(files->size() == 0);
}
SECTION("synchronization lock => ignored") {
write_file(path, "0000000000");
last_write_time(path, fs::from_unix(m_time));
auto status = bfs::status(path);
auto perms = static_cast<uint32_t>(status.permissions());
proto::set_permissions(pr_file, perms);
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
auto f = folder_info_peer->get_file_infos().by_name(file_name);
REQUIRE(f);
f->synchronizing_lock();
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(bfs::exists(path));
CHECK(!bfs::exists(model_path));
f->synchronizing_unlock();
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(!bfs::exists(path));
CHECK(!bfs::exists(model_path));
CHECK(files->size() == 0);
}
}
CHECK(!folder->get_scan_start().is_special());
CHECK(!folder->get_scan_finish().is_special());
CHECK(folder->get_scan_finish() >= folder->get_scan_start());
}
};
F().run();
}
void test_traversal() {
struct F : fixture_t {
using paths_t = std::vector<std::string>;
void on_diff(const model::diff::advance::local_update_t &diff) noexcept override {
auto file = folder_info->get_file_infos().by_uuid(diff.uuid);
auto name = file->get_name()->get_full_name();
paths.emplace_back(std::string(name));
}
void main() noexcept override {
bfs::create_directory(root_path / "a");
bfs::create_directory(root_path / "a" / "c");
bfs::create_directory(root_path / "b");
bfs::create_directory(root_path / "d");
bfs::create_directory(root_path / "d" / "d1");
bfs::create_directory(root_path / "d" / "d2");
write_file(root_path / "x.bin", "");
write_file(root_path / "y.bin", "");
write_file(root_path / "a/file.bin", "");
write_file(root_path / "a/c/file_2.bin", "");
write_file(root_path / "d/d1/file_3.bin", "");
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(files->size() == 11);
REQUIRE(paths.size() == 11);
// clang-format off
auto expected = paths_t{
"a",
"a/c",
"a/c/file_2.bin",
"a/file.bin",
"b",
"d",
"d/d1",
"d/d1/file_3.bin",
"d/d2",
"x.bin",
"y.bin",
};
// clang-format on
CHECK(paths == expected);
}
paths_t paths;
};
F().run();
}
void test_importing() {
struct F : fixture_t {
void main() noexcept override {
auto peer_short_id = my_device->device_id().get_uint();
auto modified_s = std::int64_t{12345};
auto sha256 = peer_device->device_id().get_sha256();
SECTION("regular file") {
auto block_sz = fs::block_sizes[0];
auto path = root_path / L"файл";
auto pr_file = proto::FileInfo{};
auto file_name = narrow(L"файл");
proto::set_name(pr_file, file_name);
proto::set_sequence(pr_file, 4);
auto &v = proto::get_version(pr_file);
auto &counter = proto::add_counters(v);
proto::set_id(counter, 55);
proto::set_value(counter, peer_short_id);
proto::set_modified_s(pr_file, modified_s);
auto data_1 = as_owned_bytes("12345");
auto data_2 = as_owned_bytes("67890");
auto hash_1 = utils::sha256_digest(data_1).value();
auto hash_2 = utils::sha256_digest(data_2).value();
auto b_1 = proto::BlockInfo();
proto::set_hash(b_1, hash_1);
proto::set_size(b_1, data_1.size());
auto b_2 = proto::BlockInfo();
proto::set_hash(b_2, hash_2);
proto::set_offset(b_2, data_1.size());
proto::set_size(b_2, data_2.size());
SECTION("one block file") {
proto::add_blocks(pr_file, b_1);
proto::set_size(pr_file, 5);
proto::set_block_size(pr_file, 5);
write_file(path, "12345");
bfs::last_write_time(path, from_unix(modified_s));
proto::set_permissions(pr_file, static_cast<uint32_t>(bfs::status(path).permissions()));
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(files->size() == 1);
auto f = *files->begin();
REQUIRE(f);
CHECK(f->get_size() == proto::get_size(pr_file));
REQUIRE(f->get_version().as_proto() == v);
}
SECTION("two blocks file (non-standard size)") {
proto::add_blocks(pr_file, b_1);
proto::add_blocks(pr_file, b_2);
proto::set_size(pr_file, 10);
proto::set_block_size(pr_file, 5);
write_file(path, "1234567890");
bfs::last_write_time(path, from_unix(modified_s));
proto::set_permissions(pr_file, static_cast<uint32_t>(bfs::status(path).permissions()));
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(files->size() == 1);
auto f = files->by_name(file_name);
REQUIRE(f);
CHECK(f->get_size() == proto::get_size(pr_file));
REQUIRE(f->get_version().as_proto() == v);
}
}
SECTION("directory") {
auto path = root_path / L"папка";
auto pr_file = proto::FileInfo{};
auto file_name = narrow(L"папка");
proto::set_name(pr_file, file_name);
proto::set_sequence(pr_file, 4);
proto::set_type(pr_file, proto::FileInfoType::DIRECTORY);
auto &v = proto::get_version(pr_file);
auto &counter = proto::add_counters(v);
proto::set_id(counter, 55);
proto::set_value(counter, peer_short_id);
proto::set_modified_s(pr_file, modified_s);
#ifdef SYNCSPIRIT_WIN
proto::set_no_permissions(pr_file, true);
#endif
bfs::create_directories(path);
proto::set_permissions(pr_file, static_cast<uint32_t>(bfs::status(path).permissions()));
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(files->size() == 1);
auto f = files->by_name(file_name);
REQUIRE(f);
CHECK(f->get_size() == proto::get_size(pr_file));
REQUIRE(f->get_version().as_proto() == v);
}
SECTION("deleted") {
SECTION("single deleted file") {
auto path = root_path / L"файл.bin";
auto pr_file = proto::FileInfo();
proto::set_name(pr_file, path.filename().string());
proto::set_sequence(pr_file, 4);
proto::set_type(pr_file, proto::FileInfoType::FILE);
proto::set_deleted(pr_file, true);
auto &v = proto::get_version(pr_file);
auto &counter = proto::add_counters(v);
proto::set_id(counter, 1);
proto::set_value(counter, 1);
builder->make_index(sha256, folder->get_id()).add(pr_file, peer_device).finish().apply(*sup);
builder->scan_start(folder->get_id()).apply(*sup);
auto fi_my = folder->get_folder_infos().by_device(*my_device);
auto &files_my = fi_my->get_file_infos();
REQUIRE(files_my.size() == 1);
auto file = files_my.by_name(path.filename().string());
REQUIRE(file->get_version().as_proto() == v);
CHECK(!bfs::exists(path));
}
SECTION("deleted file inside deleted dir") {
auto v = proto::Vector();
auto &counter = proto::add_counters(v);
proto::set_id(counter, 1);
proto::set_value(counter, 1);
auto dir_path = bfs::path(L"папка");
auto file_path = dir_path / L"файл.bin";
auto narrow_dir = narrow(dir_path.generic_wstring());
auto narrow_file = narrow(file_path.generic_wstring());
auto pr_dir = proto::FileInfo();
proto::set_name(pr_dir, narrow_dir);
proto::set_sequence(pr_dir, 4);
proto::set_type(pr_dir, proto::FileInfoType::DIRECTORY);
proto::set_deleted(pr_dir, true);
proto::set_version(pr_dir, v);
auto pr_file = proto::FileInfo();
proto::set_name(pr_file, narrow_file);
proto::set_sequence(pr_file, 5);
proto::set_type(pr_file, proto::FileInfoType::FILE);
proto::set_deleted(pr_file, true);
proto::set_version(pr_file, v);
builder->make_index(sha256, folder->get_id())
.add(pr_dir, peer_device)
.add(pr_file, peer_device)
.finish()
.apply(*sup);
builder->scan_start(folder->get_id()).apply(*sup);
auto &files_my = folder_info->get_file_infos();
REQUIRE(files_my.size() == 2);
auto file_1 = files_my.by_name(narrow_dir);
REQUIRE(file_1->get_version().as_proto() == v);
auto file_2 = files_my.by_name(narrow_file);
REQUIRE(file_2->get_version().as_proto() == v);
auto it = bfs::directory_iterator(root_path);
auto children_count = std::distance(it, bfs::directory_iterator());
CHECK(children_count == 0);
}
}
}
};
F().run();
}
void test_concurrency() {
static constexpr std::uint_fast32_t N = 5;
static constexpr auto M = constants::diffs_batch;
struct F : fixture_t {
void on_model_update(model::message::model_update_t &msg) noexcept override {
struct V final : model::diff::cluster_visitor_t {
using parent_t = model::diff::cluster_visitor_t;
V(int *ptr_) : ptr{ptr_} {}
outcome::result<void> operator()(const model::diff::load::interrupt_t &diff,
void *custom) noexcept override {
++(*ptr);
return diff.visit_next(*this, custom);
}
int *ptr;
};
auto v = V(&interrupts);
std::ignore = msg.payload.diff->visit(v, nullptr);
}
void main() noexcept override {
for (int i = 0; i < N; ++i) {
auto letter = static_cast<char>('a' + i);
auto dir_name = std::string_view(&letter, 1);
auto dir_path = root_path / "sub-dir" / dir_name;
bfs::create_directories(dir_path);
for (std::uint_fast32_t j = 0; j < M; ++j) {
auto file_name = fmt::format("{:03}.bin", j);
auto file_path = dir_path / file_name;
write_file(file_path, "");
}
}
builder->scan_start(folder->get_id()).apply(*sup);
REQUIRE(files->size() == 1 + N * (M + 1));
interrupts = 0;
bfs::remove_all(root_path / "sub-dir");
builder->scan_start(folder->get_id()).apply(*sup);
CHECK(interrupts >= N);
CHECK(interrupts <= N * 2);
}
int interrupts = 0;
};
F().run();
}
void test_races() {
static constexpr int N = 5;
struct F : fixture_t {
void main() noexcept override {
for (int i = 0; i < N; ++i) {
auto path = root_path / fmt::format("file-{}", i);
write_file(path, "12345");
}
SECTION("shutdown during I/O") {
auto diff = builder->scan_start(folder->get_id()).extract();
sup->send<model::payload::model_update_t>(sup->get_address(), std::move(diff), nullptr);
target->do_shutdown();
sup->do_process();
CHECK(files->size() == 0);
CHECK(hasher->digested_blocks == 0);
}
}
};
F().run();
}
int _init() {
test::init_logging();
REGISTER_TEST_CASE(test_simple, "test_simple", "[net]");
REGISTER_TEST_CASE(test_create_dir, "test_create_dir", "[net]");
REGISTER_TEST_CASE(test_no_changes, "test_no_changes", "[net]");
REGISTER_TEST_CASE(test_deleted, "test_deleted", "[net]");
REGISTER_TEST_CASE(test_changed, "test_changed", "[net]");
REGISTER_TEST_CASE(test_type_change, "test_type_change", "[net]");
REGISTER_TEST_CASE(test_resurrection, "test_resurrection", "[net]");
REGISTER_TEST_CASE(test_partial_scan, "test_partial_scan", "[net]");
REGISTER_TEST_CASE(test_scan_errors, "test_scan_errors", "[net]");
REGISTER_TEST_CASE(test_read_errors, "test_read_errors", "[net]");
REGISTER_TEST_CASE(test_leaks, "test_leaks", "[net]");
REGISTER_TEST_CASE(test_hashing_fail, "test_hashing_fail", "[net]");
REGISTER_TEST_CASE(test_incomplete, "test_incomplete", "[net]");
REGISTER_TEST_CASE(test_traversal, "test_traversal", "[net]");
REGISTER_TEST_CASE(test_importing, "test_importing", "[net]");
REGISTER_TEST_CASE(test_concurrency, "test_concurrency", "[net]");
REGISTER_TEST_CASE(test_races, "test_races", "[net]");
return 1;
}
static int v = _init();