#include "acutest/acutest.h" #include "config.hpp" #include "fs_copyfile.hpp" #include "str.hpp" #include "thread_pool.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include template bool wait_until(Predicate pred_, const int timeout_ms_ = 2000) { for(int i = 0; i < timeout_ms_; ++i) { if(pred_()) return true; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return pred_(); } void test_nop() { TEST_CHECK(true); } void test_config_bool() { ConfigBOOL v; TEST_CHECK(v.from_string("true") == 0); TEST_CHECK(v == true); TEST_CHECK(v.to_string() == "true"); TEST_CHECK(v.from_string("false") == 0); TEST_CHECK(v == false); TEST_CHECK(v.to_string() == "false"); TEST_CHECK(v.from_string("asdf") == -EINVAL); } void test_config_uint64() { ConfigU64 v; TEST_CHECK(v.from_string("0") == 0); TEST_CHECK(v == (uint64_t)0); TEST_CHECK(v.to_string() == "0"); TEST_CHECK(v.from_string("123") == 0); TEST_CHECK(v == (uint64_t)123); TEST_CHECK(v.to_string() == "123"); TEST_CHECK(v.from_string("1234567890") == 0); TEST_CHECK(v == (uint64_t)1234567890); TEST_CHECK(v.to_string() == "1234567890"); TEST_CHECK(v.from_string("asdf") == -EINVAL); } void test_config_int() { ConfigINT v; TEST_CHECK(v.from_string("0") == 0); TEST_CHECK(v == (int)0); TEST_CHECK(v.to_string() == "0"); TEST_CHECK(v.from_string("123") == 0); TEST_CHECK(v == (int)123); TEST_CHECK(v.to_string() == "123"); TEST_CHECK(v.from_string("1234567890") == 0); TEST_CHECK(v == (int)1234567890); TEST_CHECK(v.to_string() == "1234567890"); TEST_CHECK(v.from_string("asdf") == -EINVAL); } void test_config_str() { ConfigSTR v; TEST_CHECK(v.from_string("foo") == 0); TEST_CHECK(v == "foo"); TEST_CHECK(v.to_string() == "foo"); } void test_str_stuff() { std::vector v; std::set s; std::pair kv; // split() - vector v = str::split("",':'); TEST_CHECK(v.size() == 0); v = str::split("a:b:c",':'); TEST_CHECK(v.size() == 3); TEST_CHECK(v[0] == "a"); TEST_CHECK(v[1] == "b"); TEST_CHECK(v[2] == "c"); v = str::split("a::b:c",':'); TEST_CHECK(v.size() == 4); TEST_CHECK(v[0] == "a"); TEST_CHECK(v[1] == ""); TEST_CHECK(v[2] == "b"); TEST_CHECK(v[3] == "c"); v = str::split("single",':'); TEST_CHECK(v.size() == 1); TEST_CHECK(v[0] == "single"); // split_to_set() s = str::split_to_set("",':'); TEST_CHECK(s.size() == 0); s = str::split_to_set("a:b:c",':'); TEST_CHECK(s.size() == 3); TEST_CHECK(s.count("a") == 1); TEST_CHECK(s.count("b") == 1); TEST_CHECK(s.count("c") == 1); s = str::split_to_set("a:a:b",':'); TEST_CHECK(s.size() == 2); TEST_CHECK(s.count("a") == 1); TEST_CHECK(s.count("b") == 1); // split_on_null() std::string nullstr = "hello"; nullstr += '\0'; nullstr += "world"; nullstr += '\0'; nullstr += "test"; v = str::split_on_null(std::string_view(nullstr.data(), nullstr.size())); TEST_CHECK(v.size() == 3); TEST_CHECK(v[0] == "hello"); TEST_CHECK(v[1] == "world"); TEST_CHECK(v[2] == "test"); v = str::split_on_null(""); TEST_CHECK(v.size() == 0); // lsplit1() v = str::lsplit1("foo=bar=baz",'='); TEST_CHECK(v.size() == 2); TEST_CHECK(v[0] == "foo"); TEST_CHECK(v[1] == "bar=baz"); v = str::lsplit1("",'='); TEST_CHECK(v.size() == 0); v = str::lsplit1("no_delimiter",'='); TEST_CHECK(v.size() == 1); TEST_CHECK(v[0] == "no_delimiter"); // rsplit1() v = str::rsplit1("foo=bar=baz",'='); TEST_CHECK(v.size() == 2); TEST_CHECK(v[0] == "foo=bar"); TEST_CHECK(v[1] == "baz"); v = str::rsplit1("",'='); TEST_CHECK(v.size() == 0); v = str::rsplit1("no_delimiter",'='); TEST_CHECK(v.size() == 1); TEST_CHECK(v[0] == "no_delimiter"); // splitkv() kv = str::splitkv("key=value", '='); TEST_CHECK(kv.first == "key"); TEST_CHECK(kv.second == "value"); kv = str::splitkv("key=", '='); TEST_CHECK(kv.first == "key"); TEST_CHECK(kv.second == ""); kv = str::splitkv("key", '='); TEST_CHECK(kv.first == "key"); TEST_CHECK(kv.second == ""); kv = str::splitkv("", '='); TEST_CHECK(kv.first == ""); TEST_CHECK(kv.second == ""); // join() - vector with substr v = {"abc", "def", "ghi"}; std::string joined = str::join(v, 1, ':'); TEST_CHECK(joined == "bc:ef:hi"); // join() - vector joined = str::join(v, ':'); TEST_CHECK(joined == "abc:def:ghi"); v = {"single"}; joined = str::join(v, ':'); TEST_CHECK(joined == "single"); v = {}; joined = str::join(v, ':'); TEST_CHECK(joined == ""); // join() - set s = {"a", "b", "c"}; joined = str::join(s, ':'); TEST_CHECK(joined == "a:b:c"); s = {}; joined = str::join(s, ':'); TEST_CHECK(joined == ""); // longest_common_prefix_index() v = {"/foo/bar", "/foo/baz", "/foo/qux"}; TEST_CHECK(str::longest_common_prefix_index(v) == 5); v = {"/foo/bar"}; TEST_CHECK(str::longest_common_prefix_index(v) == std::string::npos); v = {}; TEST_CHECK(str::longest_common_prefix_index(v) == std::string::npos); v = {"abc", "xyz"}; TEST_CHECK(str::longest_common_prefix_index(v) == 0); // longest_common_prefix() v = {"/foo/bar", "/foo/baz"}; TEST_CHECK(str::longest_common_prefix(v) == "/foo/ba"); v = {"/foo/bar"}; // Single element - returns empty string (implementation detail) TEST_CHECK(str::longest_common_prefix(v) == ""); v = {}; TEST_CHECK(str::longest_common_prefix(v) == ""); // remove_common_prefix_and_join() v = {"/foo/bar", "/foo/baz", "/foo/qux"}; joined = str::remove_common_prefix_and_join(v, ':'); TEST_CHECK(joined == "bar:baz:qux"); v = {}; joined = str::remove_common_prefix_and_join(v, ':'); TEST_CHECK(joined == ""); // startswith() - string_view TEST_CHECK(str::startswith("hello world", "hello") == true); TEST_CHECK(str::startswith("hello world", "world") == false); TEST_CHECK(str::startswith("hello", "hello world") == false); TEST_CHECK(str::startswith("", "") == true); TEST_CHECK(str::startswith("hello", "") == true); // startswith() - char* TEST_CHECK(str::startswith("hello world", "hello") == true); TEST_CHECK(str::startswith("hello world", "world") == false); // endswith() TEST_CHECK(str::endswith("hello world", "world") == true); TEST_CHECK(str::endswith("hello world", "hello") == false); TEST_CHECK(str::endswith("hello", "hello world") == false); TEST_CHECK(str::endswith("", "") == true); TEST_CHECK(str::endswith("hello", "") == true); // trim() TEST_CHECK(str::trim(" hello ") == "hello"); TEST_CHECK(str::trim("hello") == "hello"); TEST_CHECK(str::trim(" ") == ""); TEST_CHECK(str::trim("") == ""); TEST_CHECK(str::trim("\t\nhello\r\n") == "hello"); TEST_CHECK(str::trim(" hello world ") == "hello world"); // eq() TEST_CHECK(str::eq("hello", "hello") == true); TEST_CHECK(str::eq("hello", "world") == false); TEST_CHECK(str::eq("", "") == true); // replace() TEST_CHECK(str::replace("hello world", ' ', '_') == "hello_world"); TEST_CHECK(str::replace("abc", 'x', 'y') == "abc"); TEST_CHECK(str::replace("", 'x', 'y') == ""); TEST_CHECK(str::replace("aaa", 'a', 'b') == "bbb"); // erase_fnmatches() v = {"foo.txt", "bar.cpp", "baz.h", "test.txt"}; std::vector patterns = {"*.txt"}; str::erase_fnmatches(patterns, v); TEST_CHECK(v.size() == 2); // Order may vary, just check elements exist bool has_bar_cpp = false; bool has_baz_h = false; for(const auto& s : v) { if(s == "bar.cpp") has_bar_cpp = true; if(s == "baz.h") has_baz_h = true; } TEST_CHECK(has_bar_cpp); TEST_CHECK(has_baz_h); v = {"foo.txt", "bar.cpp"}; patterns = {"*.txt", "*.cpp"}; str::erase_fnmatches(patterns, v); TEST_CHECK(v.size() == 0); v = {"foo.txt", "bar.cpp"}; patterns = {}; str::erase_fnmatches(patterns, v); TEST_CHECK(v.size() == 2); } void test_config_branches() { uint64_t minfreespace = 1234; Branches b; b.minfreespace = minfreespace; Branches::Ptr bcp0; Branches::Ptr bcp1; TEST_CHECK(b->minfreespace() == 1234); TEST_CHECK(b.to_string() == ""); // Parse initial value for branch TEST_CHECK(b.from_string(b.to_string()) == 0); bcp0 = b; TEST_CHECK(b.from_string("/foo/bar") == 0); TEST_CHECK(b.to_string() == "/foo/bar=RW"); bcp1 = b; TEST_CHECK(bcp0.get() != bcp1.get()); TEST_CHECK(b.from_string("/foo/bar=RW,1234") == 0); TEST_CHECK(b.to_string() == "/foo/bar=RW,1234"); TEST_CHECK(b.from_string("/foo/bar=RO,1234:/foo/baz=NC,4321") == 0); TEST_CHECK(b.to_string() == "/foo/bar=RO,1234:/foo/baz=NC,4321"); bcp0 = b; TEST_CHECK((*bcp0).size() == 2); TEST_CHECK((*bcp0)[0].path == "/foo/bar"); TEST_CHECK((*bcp0)[0].minfreespace() == 1234); TEST_MSG("minfreespace: expected = %lu; produced = %lu", 1234UL, (*bcp0)[0].minfreespace()); TEST_CHECK((*bcp0)[1].path == "/foo/baz"); TEST_CHECK((*bcp0)[1].minfreespace() == 4321); TEST_MSG("minfreespace: expected = %lu; produced = %lu", 4321UL, (*bcp0)[1].minfreespace()); TEST_CHECK(b.from_string("foo/bar") == 0); TEST_CHECK(b.from_string("./foo/bar") == 0); TEST_CHECK(b.from_string("./foo/bar:/bar/baz:blah/asdf") == 0); } // --------------------------------------------------------------------------- // Branch unit tests // --------------------------------------------------------------------------- void test_branch_default_ctor_mode() { Branch b; TEST_CHECK(b.mode == Branch::Mode::RW); } void test_branch_to_string_modes() { uint64_t global = 0; Branch b(global); b.path = "/mnt/disk1"; b.mode = Branch::Mode::RW; TEST_CHECK(b.to_string() == "/mnt/disk1=RW"); b.mode = Branch::Mode::RO; TEST_CHECK(b.to_string() == "/mnt/disk1=RO"); b.mode = Branch::Mode::NC; TEST_CHECK(b.to_string() == "/mnt/disk1=NC"); } void test_branch_to_string_with_minfreespace() { uint64_t global = 0; Branch b(global); b.path = "/mnt/disk1"; b.mode = Branch::Mode::RW; // While minfreespace is held by pointer (global default), it is NOT emitted TEST_CHECK(b.to_string() == "/mnt/disk1=RW"); // Once set_minfreespace is called the value is stored locally and IS emitted b.set_minfreespace(1024); TEST_CHECK(b.to_string() == "/mnt/disk1=RW,1K"); b.set_minfreespace(1024 * 1024); TEST_CHECK(b.to_string() == "/mnt/disk1=RW,1M"); b.set_minfreespace(1024 * 1024 * 1024); TEST_CHECK(b.to_string() == "/mnt/disk1=RW,1G"); b.set_minfreespace(500); TEST_CHECK(b.to_string() == "/mnt/disk1=RW,500"); } void test_branch_mode_predicates() { Branch b; b.mode = Branch::Mode::RW; TEST_CHECK(b.ro() == false); TEST_CHECK(b.nc() == false); TEST_CHECK(b.ro_or_nc() == false); b.mode = Branch::Mode::RO; TEST_CHECK(b.ro() == true); TEST_CHECK(b.nc() == false); TEST_CHECK(b.ro_or_nc() == true); b.mode = Branch::Mode::NC; TEST_CHECK(b.ro() == false); TEST_CHECK(b.nc() == true); TEST_CHECK(b.ro_or_nc() == true); } void test_branch_minfreespace_pointer_vs_value() { uint64_t global = 9999; Branch b(global); // Reads through the pointer TEST_CHECK(b.minfreespace() == 9999); // Pointer tracks changes to the referenced global global = 1234; TEST_CHECK(b.minfreespace() == 1234); // set_minfreespace switches variant to owned u64 b.set_minfreespace(42); TEST_CHECK(b.minfreespace() == 42); // Global changes no longer affect it global = 9999; TEST_CHECK(b.minfreespace() == 42); } void test_branch_copy_constructor() { uint64_t global = 100; Branch orig(global); orig.path = "/orig"; orig.mode = Branch::Mode::RO; Branch copy(orig); TEST_CHECK(copy.path == "/orig"); TEST_CHECK(copy.mode == Branch::Mode::RO); TEST_CHECK(copy.minfreespace() == 100); } void test_branch_copy_assignment() { uint64_t global = 77; Branch a(global); a.path = "/a"; a.mode = Branch::Mode::NC; a.set_minfreespace(555); Branch b; b = a; TEST_CHECK(b.path == "/a"); TEST_CHECK(b.mode == Branch::Mode::NC); TEST_CHECK(b.minfreespace() == 555); } // --------------------------------------------------------------------------- // Branches unit tests // --------------------------------------------------------------------------- void test_branches_default_minfreespace() { Branches b; // Default minfreespace propagates into each parsed branch b.minfreespace = 8192; TEST_CHECK(b.from_string("/tmp/a") == 0); Branches::Ptr p = b; TEST_CHECK((*p)[0].minfreespace() == 8192); } void test_branches_per_branch_minfreespace_overrides_default() { Branches b; b.minfreespace = 1000; TEST_CHECK(b.from_string("/tmp/a=RW,2000") == 0); Branches::Ptr p = b; TEST_CHECK((*p)[0].minfreespace() == 2000); TEST_MSG("expected 2000, got %lu", (unsigned long)(*p)[0].minfreespace()); } void test_branches_add_end_operator() { Branches b; TEST_CHECK(b.from_string("/tmp/a") == 0); TEST_CHECK(b->size() == 1); // + appends to end TEST_CHECK(b.from_string("+/tmp/b") == 0); Branches::Ptr p = b; TEST_CHECK((*p).size() == 2); TEST_CHECK((*p)[0].path == "/tmp/a"); TEST_CHECK((*p)[1].path == "/tmp/b"); // +> also appends to end TEST_CHECK(b.from_string("+>/tmp/c") == 0); p = b; TEST_CHECK((*p).size() == 3); TEST_CHECK((*p)[2].path == "/tmp/c"); } void test_branches_add_begin_operator() { Branches b; TEST_CHECK(b.from_string("/tmp/a") == 0); TEST_CHECK(b.from_string("+size() == 2); // = replaces the whole list TEST_CHECK(b.from_string("=/tmp/c") == 0); Branches::Ptr p = b; TEST_CHECK((*p).size() == 1); TEST_CHECK((*p)[0].path == "/tmp/c"); } void test_branches_erase_end_operator() { Branches b; TEST_CHECK(b.from_string("/tmp/a:/tmp/b:/tmp/c") == 0); TEST_CHECK(b.from_string("->") == 0); Branches::Ptr p = b; TEST_CHECK((*p).size() == 2); TEST_CHECK((*p).back().path == "/tmp/b"); } void test_branches_erase_begin_operator() { Branches b; TEST_CHECK(b.from_string("/tmp/a:/tmp/b:/tmp/c") == 0); TEST_CHECK(b.from_string("-<") == 0); Branches::Ptr p = b; TEST_CHECK((*p).size() == 2); TEST_CHECK((*p).front().path == "/tmp/b"); } void test_branches_erase_fnmatch_operator() { Branches b; TEST_CHECK(b.from_string("/tmp/disk1:/tmp/disk2:/mnt/ssd") == 0); // Remove branches matching /tmp/* TEST_CHECK(b.from_string("-/tmp/*") == 0); Branches::Ptr p = b; TEST_CHECK((*p).size() == 1); TEST_CHECK((*p)[0].path == "/mnt/ssd"); } void test_branches_erase_fnmatch_multiple_patterns() { Branches b; TEST_CHECK(b.from_string("/tmp/a:/tmp/b:/mnt/c:/mnt/d") == 0); TEST_CHECK(b.from_string("-/tmp/a:/mnt/d") == 0); Branches::Ptr p = b; TEST_CHECK((*p).size() == 2); TEST_CHECK((*p)[0].path == "/tmp/b"); TEST_CHECK((*p)[1].path == "/mnt/c"); } void test_branches_invalid_mode() { Branches b; TEST_CHECK(b.from_string("/tmp/a=XX") == -EINVAL); } void test_branches_invalid_minfreespace() { Branches b; TEST_CHECK(b.from_string("/tmp/a=RW,notanumber") == -EINVAL); } void test_branches_to_paths() { Branches b; TEST_CHECK(b.from_string("/tmp/a:/tmp/b:/tmp/c") == 0); Branches::Ptr p = b; std::vector paths = (*p).to_paths(); TEST_CHECK(paths.size() == 3); TEST_CHECK(paths[0] == "/tmp/a"); TEST_CHECK(paths[1] == "/tmp/b"); TEST_CHECK(paths[2] == "/tmp/c"); StrVec sv; (*p).to_paths(sv); TEST_CHECK(sv.size() == 3); TEST_CHECK(sv[0] == "/tmp/a"); TEST_CHECK(sv[1] == "/tmp/b"); TEST_CHECK(sv[2] == "/tmp/c"); } void test_branches_cow_semantics() { Branches b; TEST_CHECK(b.from_string("/tmp/a") == 0); Branches::Ptr snap0 = b; TEST_CHECK(b.from_string("+/tmp/b") == 0); Branches::Ptr snap1 = b; // Each mutation produces a new Impl pointer TEST_CHECK(snap0.get() != snap1.get()); // snap0 is unchanged TEST_CHECK((*snap0).size() == 1); TEST_CHECK((*snap1).size() == 2); } void test_branches_mode_round_trip() { Branches b; TEST_CHECK(b.from_string("/tmp/a=RO:/tmp/b=NC:/tmp/c=RW") == 0); TEST_CHECK(b.to_string() == "/tmp/a=RO:/tmp/b=NC:/tmp/c=RW"); Branches::Ptr p = b; TEST_CHECK((*p)[0].mode == Branch::Mode::RO); TEST_CHECK((*p)[1].mode == Branch::Mode::NC); TEST_CHECK((*p)[2].mode == Branch::Mode::RW); } void test_branches_srcmounts_to_string() { Branches b; TEST_CHECK(b.from_string("/tmp/a=RO,1234:/tmp/b=NC") == 0); SrcMounts sm(b); // SrcMounts::to_string emits only paths, no mode or minfreespace TEST_CHECK(sm.to_string() == "/tmp/a:/tmp/b"); } void test_branches_srcmounts_empty() { Branches b; SrcMounts sm(b); TEST_CHECK(sm.to_string() == ""); } // --------------------------------------------------------------------------- // Branch::to_string humanization edge cases // --------------------------------------------------------------------------- void test_branch_to_string_1t() { uint64_t global = 0; Branch b(global); b.path = "/mnt/disk1"; b.mode = Branch::Mode::RW; b.set_minfreespace(1024ULL * 1024 * 1024 * 1024); TEST_CHECK(b.to_string() == "/mnt/disk1=RW,1T"); b.set_minfreespace(2ULL * 1024 * 1024 * 1024 * 1024); TEST_CHECK(b.to_string() == "/mnt/disk1=RW,2T"); } void test_branch_to_string_non_exact_multiple() { // 1536 = 1.5K: not divisible by any tier, emitted as raw decimal uint64_t global = 0; Branch b(global); b.path = "/mnt/disk1"; b.mode = Branch::Mode::RW; b.set_minfreespace(1536); TEST_CHECK(b.to_string() == "/mnt/disk1=RW,1536"); // 1537 also not exact b.set_minfreespace(1537); TEST_CHECK(b.to_string() == "/mnt/disk1=RW,1537"); } void test_branch_to_string_zero_owned_minfreespace() { // Default Branch() holds u64(0) in variant — to_string WILL emit ",0" Branch b; b.path = "/mnt/disk1"; b.mode = Branch::Mode::RW; TEST_CHECK(b.to_string() == "/mnt/disk1=RW,0"); } void test_branch_copy_pointer_aliasing() { // A copy of a pointer-backed Branch shares the same pointer — // mutating the original global value is seen by both. uint64_t global = 1024; Branch orig(global); orig.path = "/mnt/a"; orig.mode = Branch::Mode::RW; Branch copy = orig; TEST_CHECK(orig.minfreespace() == 1024); TEST_CHECK(copy.minfreespace() == 1024); // Both see the updated value through the shared pointer global = 2048; TEST_CHECK(orig.minfreespace() == 2048); TEST_CHECK(copy.minfreespace() == 2048); } // --------------------------------------------------------------------------- // parse_mode: case-sensitivity // --------------------------------------------------------------------------- void test_branches_mode_case_sensitivity() { Branches b; // Only exact uppercase is accepted TEST_CHECK(b.from_string("/tmp/a=rw") == -EINVAL); TEST_CHECK(b.from_string("/tmp/a=ro") == -EINVAL); TEST_CHECK(b.from_string("/tmp/a=nc") == -EINVAL); TEST_CHECK(b.from_string("/tmp/a=Rw") == -EINVAL); TEST_CHECK(b.from_string("/tmp/a=RW") == 0); } // --------------------------------------------------------------------------- // parse_minfreespace: suffix variants // --------------------------------------------------------------------------- void test_branches_minfreespace_lowercase_suffixes() { Branches b; Branches::Ptr p; // Lowercase k/m/g/t must be accepted and multiply correctly TEST_CHECK(b.from_string("/tmp/a=RW,1k") == 0); p = b; TEST_CHECK((*p)[0].minfreespace() == 1024ULL); TEST_CHECK(b.from_string("/tmp/a=RW,1m") == 0); p = b; TEST_CHECK((*p)[0].minfreespace() == 1024ULL * 1024); TEST_CHECK(b.from_string("/tmp/a=RW,1g") == 0); p = b; TEST_CHECK((*p)[0].minfreespace() == 1024ULL * 1024 * 1024); TEST_CHECK(b.from_string("/tmp/a=RW,1t") == 0); p = b; TEST_CHECK((*p)[0].minfreespace() == 1024ULL * 1024 * 1024 * 1024); } void test_branches_minfreespace_bytes_suffix() { Branches b; Branches::Ptr p; // 'B' / 'b' suffix means bytes (no multiplication) TEST_CHECK(b.from_string("/tmp/a=RW,1024B") == 0); p = b; TEST_CHECK((*p)[0].minfreespace() == 1024ULL); TEST_CHECK(b.from_string("/tmp/a=RW,1024b") == 0); p = b; TEST_CHECK((*p)[0].minfreespace() == 1024ULL); } void test_branches_minfreespace_zero() { Branches b; TEST_CHECK(b.from_string("/tmp/a=RW,0") == 0); Branches::Ptr p = b; TEST_CHECK((*p)[0].minfreespace() == 0ULL); // to_string should reproduce the zero TEST_CHECK(p->to_string() == "/tmp/a=RW,0"); } void test_branches_minfreespace_negative_invalid() { Branches b; TEST_CHECK(b.from_string("/tmp/a=RW,-1") == -EINVAL); } void test_branches_minfreespace_overflow() { Branches b; // 16777216T overflows u64 (16777216 * 2^40 > 2^64-1) TEST_CHECK(b.from_string("/tmp/a=RW,16777216T") == -EOVERFLOW); } // --------------------------------------------------------------------------- // Minfreespace round-trip through Branches // --------------------------------------------------------------------------- void test_branches_minfreespace_round_trip() { Branches b; // Parse 1K — should store 1024, to_string should emit "1K" TEST_CHECK(b.from_string("/tmp/a=RW,1K") == 0); { Branches::Ptr p = b; TEST_CHECK((*p)[0].minfreespace() == 1024ULL); TEST_CHECK(p->to_string() == "/tmp/a=RW,1K"); // Re-parsing the output should produce the same state TEST_CHECK(b.from_string(p->to_string()) == 0); } { Branches::Ptr p = b; TEST_CHECK((*p)[0].minfreespace() == 1024ULL); } } void test_branches_minfreespace_round_trip_1t() { Branches b; TEST_CHECK(b.from_string("/tmp/a=RW,1T") == 0); Branches::Ptr p = b; TEST_CHECK((*p)[0].minfreespace() == 1024ULL * 1024 * 1024 * 1024); TEST_CHECK(p->to_string() == "/tmp/a=RW,1T"); } // --------------------------------------------------------------------------- // Instruction dispatch edge cases // --------------------------------------------------------------------------- void test_branches_clear_with_equals_alone() { Branches b; TEST_CHECK(b.from_string("/tmp/a:/tmp/b") == 0); TEST_CHECK(b->size() == 2); // "=" alone clears the list TEST_CHECK(b.from_string("=") == 0); TEST_CHECK(b->empty()); } void test_branches_empty_string_clears_same_as_equals() { // "" and "=" both dispatch to l::set — should produce identical result Branches b1, b2; TEST_CHECK(b1.from_string("/tmp/a") == 0); TEST_CHECK(b2.from_string("/tmp/a") == 0); TEST_CHECK(b1.from_string("=") == 0); TEST_CHECK(b2.from_string("") == 0); TEST_CHECK(b1->empty()); TEST_CHECK(b2->empty()); } void test_branches_plus_and_plus_greater_equivalent() { // "+" and "+>" both dispatch to l::add_end Branches b1, b2; TEST_CHECK(b1.from_string("/tmp/a") == 0); TEST_CHECK(b2.from_string("/tmp/a") == 0); TEST_CHECK(b1.from_string("+/tmp/b") == 0); TEST_CHECK(b2.from_string("+>/tmp/b") == 0); TEST_CHECK(b1->to_string() == b2->to_string()); } void test_branches_unknown_instruction_einval() { Branches b; // "++" is not a recognized instruction TEST_CHECK(b.from_string("++-/tmp/a") == -EINVAL); // "+-" is not recognized either TEST_CHECK(b.from_string("+-/tmp/a") == -EINVAL); } // --------------------------------------------------------------------------- // parse_branch edge cases // --------------------------------------------------------------------------- void test_branches_empty_options_after_equals_einval() { Branches b; // "/tmp/a=" — empty options field (no mode) is invalid TEST_CHECK(b.from_string("/tmp/a=") == -EINVAL); } void test_branches_path_with_equals_in_name() { Branches b; // rsplit1 splits on the LAST '=' so "/path/with=sign=RW" works TEST_CHECK(b.from_string("/tmp/with=equals=RW") == 0); Branches::Ptr p = b; TEST_CHECK((*p)[0].path == "/tmp/with=equals"); TEST_CHECK((*p)[0].mode == Branch::Mode::RW); } // --------------------------------------------------------------------------- // add_begin / add_end with multiple colon-separated paths // --------------------------------------------------------------------------- void test_branches_add_end_multiple_paths() { Branches b; TEST_CHECK(b.from_string("/tmp/a") == 0); // "+>" with two paths should append both in order TEST_CHECK(b.from_string("+>/tmp/b:/tmp/c") == 0); Branches::Ptr p = b; TEST_CHECK((*p).size() == 3); TEST_CHECK((*p)[0].path == "/tmp/a"); TEST_CHECK((*p)[1].path == "/tmp/b"); TEST_CHECK((*p)[2].path == "/tmp/c"); } void test_branches_add_begin_multiple_paths() { Branches b; TEST_CHECK(b.from_string("/tmp/a") == 0); // "+<" with two paths should prepend both in order TEST_CHECK(b.from_string("+size() == 2); } void test_branches_erase_wildcard_removes_all() { Branches b; TEST_CHECK(b.from_string("/tmp/a:/tmp/b:/tmp/c") == 0); // "-*" — '*' matches everything (no FNM_PATHNAME, so '*' matches '/') TEST_CHECK(b.from_string("-*") == 0); TEST_CHECK(b->empty()); } void test_branches_erase_nonmatching_pattern_noop() { Branches b; TEST_CHECK(b.from_string("/tmp/a:/tmp/b") == 0); // Pattern that matches nothing — list unchanged TEST_CHECK(b.from_string("-/mnt/*") == 0); TEST_CHECK(b->size() == 2); } void test_branches_erase_multi_pattern_first_miss_second_hit() { Branches b; TEST_CHECK(b.from_string("/tmp/a:/mnt/b") == 0); // First pattern matches nothing, second matches /mnt/b TEST_CHECK(b.from_string("-/nonexist/*:/mnt/*") == 0); Branches::Ptr p = b; TEST_CHECK((*p).size() == 1); TEST_CHECK((*p)[0].path == "/tmp/a"); } // --------------------------------------------------------------------------- // erase_begin / erase_end on single-element list // --------------------------------------------------------------------------- void test_branches_erase_end_single_element() { Branches b; TEST_CHECK(b.from_string("/tmp/a") == 0); TEST_CHECK(b.from_string("->") == 0); TEST_CHECK(b->empty()); } void test_branches_erase_begin_single_element() { Branches b; TEST_CHECK(b.from_string("/tmp/a") == 0); TEST_CHECK(b.from_string("-<") == 0); TEST_CHECK(b->empty()); } void test_branches_erase_end_empty_returns_enoent() { Branches b; TEST_CHECK(b.from_string("->") == -ENOENT); TEST_CHECK(b->empty()); } void test_branches_erase_begin_empty_returns_enoent() { Branches b; TEST_CHECK(b.from_string("-<") == -ENOENT); TEST_CHECK(b->empty()); } // --------------------------------------------------------------------------- // Operation sequences // --------------------------------------------------------------------------- void test_branches_clear_then_add() { Branches b; TEST_CHECK(b.from_string("/tmp/a:/tmp/b") == 0); TEST_CHECK(b.from_string("=") == 0); TEST_CHECK(b->empty()); TEST_CHECK(b.from_string("+/tmp/c") == 0); Branches::Ptr p = b; TEST_CHECK((*p).size() == 1); TEST_CHECK((*p)[0].path == "/tmp/c"); } void test_branches_add_begin_then_end_then_erase_both() { Branches b; TEST_CHECK(b.from_string("/tmp/b") == 0); TEST_CHECK(b.from_string("+/tmp/c") == 0); // append { Branches::Ptr p = b; TEST_CHECK((*p).size() == 3); TEST_CHECK((*p)[0].path == "/tmp/a"); TEST_CHECK((*p)[1].path == "/tmp/b"); TEST_CHECK((*p)[2].path == "/tmp/c"); } TEST_CHECK(b.from_string("-<") == 0); // remove first TEST_CHECK(b.from_string("->") == 0); // remove last { Branches::Ptr p = b; TEST_CHECK((*p).size() == 1); TEST_CHECK((*p)[0].path == "/tmp/b"); } } void test_branches_error_in_multipath_add_leaves_list_unchanged() { Branches b; TEST_CHECK(b.from_string("/tmp/a") == 0); // Second path has invalid mode — the whole add should fail atomically TEST_CHECK(b.from_string("+/tmp/b:/tmp/c=XX") == -EINVAL); // Original list unchanged Branches::Ptr p = b; TEST_CHECK((*p).size() == 1); TEST_CHECK((*p)[0].path == "/tmp/a"); } void test_branches_error_in_set_leaves_list_unchanged() { Branches b; TEST_CHECK(b.from_string("/tmp/a") == 0); // Invalid set — list should be unchanged TEST_CHECK(b.from_string("=/tmp/b=BADMODE") == -EINVAL); Branches::Ptr p = b; TEST_CHECK((*p).size() == 1); TEST_CHECK((*p)[0].path == "/tmp/a"); } // --------------------------------------------------------------------------- // Branches::minfreespace live tracking // --------------------------------------------------------------------------- void test_branches_global_minfreespace_live_tracking() { Branches b; b.from_string("/tmp/a"); // Branch uses pointer to Branches::minfreespace — tracks it live uint64_t initial = b.minfreespace; { Branches::Ptr p = b; TEST_CHECK((*p)[0].minfreespace() == initial); } b.minfreespace = 999999; { Branches::Ptr p = b; TEST_CHECK((*p)[0].minfreespace() == 999999); } } // --------------------------------------------------------------------------- // SrcMounts: from_string stub and live reflection of Branches mutations // --------------------------------------------------------------------------- void test_branches_srcmounts_from_string_noop() { Branches b; TEST_CHECK(b.from_string("/tmp/a:/tmp/b") == 0); SrcMounts sm(b); // from_string on SrcMounts is a no-op stub — always returns 0, no change TEST_CHECK(sm.from_string("/tmp/c:/tmp/d") == 0); // Branches should be unchanged (still /tmp/a:/tmp/b) TEST_CHECK(sm.to_string() == "/tmp/a:/tmp/b"); } void test_branches_srcmounts_reflects_mutations() { Branches b; TEST_CHECK(b.from_string("/tmp/a") == 0); SrcMounts sm(b); TEST_CHECK(sm.to_string() == "/tmp/a"); // Mutate Branches; SrcMounts::to_string should reflect the new state TEST_CHECK(b.from_string("+/tmp/b") == 0); TEST_CHECK(sm.to_string() == "/tmp/a:/tmp/b"); TEST_CHECK(b.from_string("->") == 0); TEST_CHECK(sm.to_string() == "/tmp/a"); } void test_config_cachefiles() { CacheFiles cf; TEST_CHECK(cf.from_string("off") == 0); TEST_CHECK(cf.to_string() == "off"); TEST_CHECK(cf.from_string("partial") == 0); TEST_CHECK(cf.to_string() == "partial"); TEST_CHECK(cf.from_string("full") == 0); TEST_CHECK(cf.to_string() == "full"); TEST_CHECK(cf.from_string("auto-full") == 0); TEST_CHECK(cf.to_string() == "auto-full"); TEST_CHECK(cf.from_string("foobar") == -EINVAL); } void test_config_inodecalc() { InodeCalc ic("passthrough"); TEST_CHECK(ic.from_string("passthrough") == 0); TEST_CHECK(ic.to_string() == "passthrough"); TEST_CHECK(ic.from_string("path-hash") == 0); TEST_CHECK(ic.to_string() == "path-hash"); TEST_CHECK(ic.from_string("devino-hash") == 0); TEST_CHECK(ic.to_string() == "devino-hash"); TEST_CHECK(ic.from_string("hybrid-hash") == 0); TEST_CHECK(ic.to_string() == "hybrid-hash"); TEST_CHECK(ic.from_string("path-hash32") == 0); TEST_CHECK(ic.to_string() == "path-hash32"); TEST_CHECK(ic.from_string("devino-hash32") == 0); TEST_CHECK(ic.to_string() == "devino-hash32"); TEST_CHECK(ic.from_string("hybrid-hash32") == 0); TEST_CHECK(ic.to_string() == "hybrid-hash32"); TEST_CHECK(ic.from_string("asdf") == -EINVAL); } void test_config_moveonenospc() { MoveOnENOSPC m(false); TEST_CHECK(m.to_string() == "false"); TEST_CHECK(m.from_string("mfs") == 0); TEST_CHECK(m.to_string() == "mfs"); TEST_CHECK(m.from_string("mspmfs") == 0); TEST_CHECK(m.to_string() == "mspmfs"); TEST_CHECK(m.from_string("true") == 0); TEST_CHECK(m.to_string() == "pfrd"); TEST_CHECK(m.from_string("blah") == -EINVAL); } void test_config_nfsopenhack() { NFSOpenHack n; TEST_CHECK(n.from_string("off") == 0); TEST_CHECK(n.to_string() == "off"); TEST_CHECK(n == NFSOpenHack::ENUM::OFF); TEST_CHECK(n.from_string("git") == 0); TEST_CHECK(n.to_string() == "git"); TEST_CHECK(n == NFSOpenHack::ENUM::GIT); TEST_CHECK(n.from_string("all") == 0); TEST_CHECK(n.to_string() == "all"); TEST_CHECK(n == NFSOpenHack::ENUM::ALL); } void test_config_readdir() { } void test_config_statfs() { StatFS s; TEST_CHECK(s.from_string("base") == 0); TEST_CHECK(s.to_string() == "base"); TEST_CHECK(s == StatFS::ENUM::BASE); TEST_CHECK(s.from_string("full") == 0); TEST_CHECK(s.to_string() == "full"); TEST_CHECK(s == StatFS::ENUM::FULL); TEST_CHECK(s.from_string("asdf") == -EINVAL); } void test_config_statfs_ignore() { StatFSIgnore s; TEST_CHECK(s.from_string("none") == 0); TEST_CHECK(s.to_string() == "none"); TEST_CHECK(s == StatFSIgnore::ENUM::NONE); TEST_CHECK(s.from_string("ro") == 0); TEST_CHECK(s.to_string() == "ro"); TEST_CHECK(s == StatFSIgnore::ENUM::RO); TEST_CHECK(s.from_string("nc") == 0); TEST_CHECK(s.to_string() == "nc"); TEST_CHECK(s == StatFSIgnore::ENUM::NC); TEST_CHECK(s.from_string("asdf") == -EINVAL); } void test_config_xattr() { XAttr x; TEST_CHECK(x.from_string("passthrough") == 0); TEST_CHECK(x.to_string() == "passthrough"); TEST_CHECK(x == XAttr::ENUM::PASSTHROUGH); TEST_CHECK(x.from_string("noattr") == 0); TEST_CHECK(x.to_string() == "noattr"); TEST_CHECK(x == XAttr::ENUM::NOATTR); TEST_CHECK(x.from_string("nosys") == 0); TEST_CHECK(x.to_string() == "nosys"); TEST_CHECK(x == XAttr::ENUM::NOSYS); TEST_CHECK(x.from_string("asdf") == -EINVAL); } void test_config() { Config cfg; TEST_CHECK(cfg.set("async-read","true") == 0); } // ===================================================================== // ThreadPool tests // ===================================================================== void test_tp_construct_default() { ThreadPool tp(2, 4, "test.default"); auto threads = tp.threads(); TEST_CHECK(threads.size() == 2); } void test_tp_construct_named() { ThreadPool tp(1, 2, "test.named"); auto threads = tp.threads(); TEST_CHECK(threads.size() == 1); } void test_tp_construct_zero_threads_throws() { bool threw = false; try { ThreadPool tp(0, 4, "test.zero"); } catch(const std::runtime_error &) { threw = true; } TEST_CHECK(threw); } void test_tp_enqueue_work() { ThreadPool tp(2, 4, "test.ew"); std::atomic counter{0}; for(int i = 0; i < 10; ++i) tp.enqueue_work([&counter](){ counter.fetch_add(1); }); // wait for completion for(int i = 0; i < 1000 && counter.load() < 10; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(counter.load() == 10); } void test_tp_enqueue_work_with_ptoken() { ThreadPool tp(2, 4, "test.ewp"); std::atomic counter{0}; auto ptok = tp.ptoken(); for(int i = 0; i < 10; ++i) tp.enqueue_work(ptok, [&counter](){ counter.fetch_add(1); }); for(int i = 0; i < 1000 && counter.load() < 10; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(counter.load() == 10); } void test_tp_try_enqueue_work() { // Use 1 thread and a small queue depth of 2 ThreadPool tp(1, 2, "test.tew"); std::atomic counter{0}; // These should succeed (queue has room) bool ok = tp.try_enqueue_work([&counter](){ counter.fetch_add(1); }); TEST_CHECK(ok); for(int i = 0; i < 1000 && counter.load() < 1; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(counter.load() == 1); } void test_tp_try_enqueue_work_with_ptoken() { ThreadPool tp(1, 2, "test.tewp"); std::atomic counter{0}; auto ptok = tp.ptoken(); bool ok = tp.try_enqueue_work(ptok, [&counter](){ counter.fetch_add(1); }); TEST_CHECK(ok); for(int i = 0; i < 1000 && counter.load() < 1; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(counter.load() == 1); } void test_tp_try_enqueue_work_for() { ThreadPool tp(1, 2, "test.tewf"); std::atomic counter{0}; bool ok = tp.try_enqueue_work_for(100000, // 100ms [&counter](){ counter.fetch_add(1); }); TEST_CHECK(ok); for(int i = 0; i < 1000 && counter.load() < 1; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(counter.load() == 1); } void test_tp_try_enqueue_work_for_with_ptoken() { ThreadPool tp(1, 2, "test.tewfp"); std::atomic counter{0}; auto ptok = tp.ptoken(); bool ok = tp.try_enqueue_work_for(ptok, 100000, // 100ms [&counter](){ counter.fetch_add(1); }); TEST_CHECK(ok); for(int i = 0; i < 1000 && counter.load() < 1; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(counter.load() == 1); } void test_tp_enqueue_task_int() { ThreadPool tp(2, 4, "test.eti"); auto future = tp.enqueue_task([]() -> int { return 42; }); TEST_CHECK(future.get() == 42); } void test_tp_enqueue_task_void() { ThreadPool tp(2, 4, "test.etv"); std::atomic ran{false}; auto future = tp.enqueue_task([&ran]() { ran.store(true); }); future.get(); // should not throw TEST_CHECK(ran.load()); } void test_tp_enqueue_task_string() { ThreadPool tp(2, 4, "test.ets"); auto future = tp.enqueue_task([]() -> std::string { return "hello"; }); TEST_CHECK(future.get() == "hello"); } void test_tp_enqueue_task_exception() { ThreadPool tp(2, 4, "test.ete"); auto future = tp.enqueue_task([]() -> int { throw std::runtime_error("task error"); return 0; }); bool caught = false; try { future.get(); } catch(const std::runtime_error &e) { caught = true; TEST_CHECK(std::string(e.what()) == "task error"); } TEST_CHECK(caught); } void test_tp_threads_returns_correct_count() { ThreadPool tp(4, 8, "test.thr"); auto threads = tp.threads(); TEST_CHECK(threads.size() == 4); // All thread IDs should be non-zero for(auto t : threads) TEST_CHECK(t != 0); } void test_tp_threads_unique_ids() { ThreadPool tp(4, 8, "test.uid"); auto threads = tp.threads(); // All thread IDs should be unique for(std::size_t i = 0; i < threads.size(); ++i) for(std::size_t j = i + 1; j < threads.size(); ++j) TEST_CHECK(threads[i] != threads[j]); } void test_tp_add_thread() { ThreadPool tp(2, 4, "test.add"); TEST_CHECK(tp.threads().size() == 2); int rv = tp.add_thread(); TEST_CHECK(rv == 0); TEST_CHECK(tp.threads().size() == 3); } void test_tp_remove_thread() { ThreadPool tp(3, 4, "test.rm"); TEST_CHECK(tp.threads().size() == 3); int rv = tp.remove_thread(); TEST_CHECK(rv == 0); TEST_CHECK(tp.threads().size() == 2); } void test_tp_remove_thread_refuses_last() { ThreadPool tp(1, 4, "test.rmlast"); TEST_CHECK(tp.threads().size() == 1); int rv = tp.remove_thread(); TEST_CHECK(rv == -EINVAL); TEST_CHECK(tp.threads().size() == 1); } void test_tp_set_threads_grow() { ThreadPool tp(2, 8, "test.grow"); TEST_CHECK(tp.threads().size() == 2); int rv = tp.set_threads(4); TEST_CHECK(rv == 0); TEST_CHECK(tp.threads().size() == 4); } void test_tp_set_threads_shrink() { ThreadPool tp(4, 8, "test.shrink"); TEST_CHECK(tp.threads().size() == 4); int rv = tp.set_threads(2); TEST_CHECK(rv == 0); TEST_CHECK(tp.threads().size() == 2); } void test_tp_set_threads_zero() { ThreadPool tp(2, 4, "test.sz"); int rv = tp.set_threads(0); TEST_CHECK(rv == -EINVAL); TEST_CHECK(tp.threads().size() == 2); } void test_tp_set_threads_same() { ThreadPool tp(3, 4, "test.same"); int rv = tp.set_threads(3); TEST_CHECK(rv == 0); TEST_CHECK(tp.threads().size() == 3); } void test_tp_work_after_add_thread() { ThreadPool tp(1, 4, "test.waat"); tp.add_thread(); tp.add_thread(); std::atomic counter{0}; const int N = 20; for(int i = 0; i < N; ++i) tp.enqueue_work([&counter](){ counter.fetch_add(1); }); for(int i = 0; i < 2000 && counter.load() < N; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(counter.load() == N); } void test_tp_work_after_remove_thread() { ThreadPool tp(3, 4, "test.wart"); tp.remove_thread(); std::atomic counter{0}; const int N = 10; for(int i = 0; i < N; ++i) tp.enqueue_work([&counter](){ counter.fetch_add(1); }); for(int i = 0; i < 2000 && counter.load() < N; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(counter.load() == N); } void test_tp_worker_exception_no_crash() { ThreadPool tp(2, 4, "test.exc"); // Enqueue work that throws -- pool should not crash tp.enqueue_work([](){ throw std::runtime_error("deliberate error"); }); // Enqueue normal work after the exception std::atomic ran{false}; tp.enqueue_work([&ran](){ ran.store(true); }); for(int i = 0; i < 1000 && !ran.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(ran.load()); } void test_tp_concurrent_enqueue() { ThreadPool tp(4, 64, "test.conc"); std::atomic counter{0}; const int PRODUCERS = 4; const int ITEMS_PER = 50; const int TOTAL = PRODUCERS * ITEMS_PER; std::vector producers; for(int p = 0; p < PRODUCERS; ++p) { producers.emplace_back([&tp, &counter]() { auto ptok = tp.ptoken(); for(int i = 0; i < ITEMS_PER; ++i) tp.enqueue_work(ptok, [&counter](){ counter.fetch_add(1); }); }); } for(auto &t : producers) t.join(); for(int i = 0; i < 5000 && counter.load() < TOTAL; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(counter.load() == TOTAL); } void test_tp_enqueue_task_multiple() { ThreadPool tp(4, 16, "test.etm"); std::vector> futures; const int N = 20; for(int i = 0; i < N; ++i) futures.push_back(tp.enqueue_task([i]() -> int { return i * i; })); for(int i = 0; i < N; ++i) TEST_CHECK(futures[i].get() == i * i); } void test_tp_ptoken_creation() { ThreadPool tp(2, 4, "test.ptok"); // Multiple ptokens should be independently usable auto ptok1 = tp.ptoken(); auto ptok2 = tp.ptoken(); std::atomic counter{0}; tp.enqueue_work(ptok1, [&counter](){ counter.fetch_add(1); }); tp.enqueue_work(ptok2, [&counter](){ counter.fetch_add(1); }); for(int i = 0; i < 1000 && counter.load() < 2; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); TEST_CHECK(counter.load() == 2); } void test_tp_destruction_drains_queue() { std::atomic counter{0}; { ThreadPool tp(2, 64, "test.drain"); for(int i = 0; i < 50; ++i) tp.enqueue_work([&counter](){ counter.fetch_add(1); }); // destructor runs here -- should drain all queued work } TEST_CHECK(counter.load() == 50); } void test_tp_move_only_callable() { ThreadPool tp(2, 4, "test.moc"); auto ptr = std::make_unique(99); auto future = tp.enqueue_task([p = std::move(ptr)]() -> int { return *p; }); TEST_CHECK(future.get() == 99); } void test_tp_work_ordering_single_thread() { // With a single thread, work should execute in FIFO order ThreadPool tp(1, 16, "test.order"); std::vector results; std::mutex mtx; const int N = 10; for(int i = 0; i < N; ++i) tp.enqueue_work([i, &results, &mtx](){ std::lock_guard lk(mtx); results.push_back(i); }); // Wait for all work to complete for(int i = 0; i < 2000; ++i) { { std::lock_guard lk(mtx); if((int)results.size() == N) break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } std::lock_guard lk(mtx); TEST_CHECK((int)results.size() == N); for(int i = 0; i < N; ++i) TEST_CHECK(results[i] == i); } void test_tp_try_enqueue_work_fails_when_full() { ThreadPool tp(1, 1, "test.tewfull"); std::atomic release{false}; std::atomic worker_started{false}; std::atomic ran{0}; tp.enqueue_work([&release, &worker_started, &ran]() { worker_started.store(true); while(!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); ran.fetch_add(1); }); TEST_CHECK(wait_until([&worker_started](){ return worker_started.load(); })); tp.enqueue_work([&ran](){ ran.fetch_add(1); }); bool ok = tp.try_enqueue_work([&ran](){ ran.fetch_add(1); }); TEST_CHECK(!ok); release.store(true); TEST_CHECK(wait_until([&ran](){ return ran.load() == 2; })); TEST_CHECK(ran.load() == 2); } void test_tp_try_enqueue_work_for_times_out_when_full() { ThreadPool tp(1, 1, "test.tewftimeout"); std::atomic release{false}; std::atomic worker_started{false}; std::atomic ran{0}; tp.enqueue_work([&release, &worker_started, &ran]() { worker_started.store(true); while(!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); ran.fetch_add(1); }); TEST_CHECK(wait_until([&worker_started](){ return worker_started.load(); })); tp.enqueue_work([&ran](){ ran.fetch_add(1); }); auto start = std::chrono::steady_clock::now(); bool ok = tp.try_enqueue_work_for(20000, [&ran](){ ran.fetch_add(1); }); auto end = std::chrono::steady_clock::now(); TEST_CHECK(!ok); auto elapsed_us = std::chrono::duration_cast(end - start).count(); TEST_CHECK(elapsed_us >= 10000); release.store(true); TEST_CHECK(wait_until([&ran](){ return ran.load() == 2; })); TEST_CHECK(ran.load() == 2); } void test_tp_enqueue_work_blocks_until_slot_available() { ThreadPool tp(1, 1, "test.ewblocks"); std::atomic release{false}; std::atomic worker_started{false}; std::atomic producer_returned{false}; std::atomic ran{0}; tp.enqueue_work([&release, &worker_started, &ran]() { worker_started.store(true); while(!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); ran.fetch_add(1); }); TEST_CHECK(wait_until([&worker_started](){ return worker_started.load(); })); tp.enqueue_work([&ran](){ ran.fetch_add(1); }); std::thread producer([&tp, &ran, &producer_returned]() { tp.enqueue_work([&ran](){ ran.fetch_add(1); }); producer_returned.store(true); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); TEST_CHECK(!producer_returned.load()); release.store(true); TEST_CHECK(wait_until([&producer_returned](){ return producer_returned.load(); })); producer.join(); TEST_CHECK(wait_until([&ran](){ return ran.load() == 3; })); TEST_CHECK(ran.load() == 3); } void test_tp_worker_nonstd_exception_no_crash() { ThreadPool tp(2, 4, "test.nsexc"); tp.enqueue_work([](){ throw 7; }); std::atomic ran{false}; tp.enqueue_work([&ran](){ ran.store(true); }); TEST_CHECK(wait_until([&ran](){ return ran.load(); })); TEST_CHECK(ran.load()); } void test_tp_remove_thread_under_load() { ThreadPool tp(4, 64, "test.rmuload"); std::atomic counter{0}; const int N = 300; for(int i = 0; i < N; ++i) tp.enqueue_work([&counter]() { std::this_thread::sleep_for(std::chrono::milliseconds(1)); counter.fetch_add(1); }); int rv1 = -1; int rv2 = -1; std::thread remover([&tp, &rv1, &rv2]() { rv1 = tp.remove_thread(); rv2 = tp.remove_thread(); }); remover.join(); TEST_CHECK(rv1 == 0); TEST_CHECK(rv2 == 0); TEST_CHECK(tp.threads().size() == 2); TEST_CHECK(wait_until([&counter, N](){ return counter.load() == N; }, 10000)); TEST_CHECK(counter.load() == N); std::atomic post_ok{false}; tp.enqueue_work([&post_ok](){ post_ok.store(true); }); TEST_CHECK(wait_until([&post_ok](){ return post_ok.load(); })); } void test_tp_set_threads_concurrent_with_enqueue() { ThreadPool tp(3, 128, "test.stcwe"); const int PRODUCERS = 4; const int PER_PRODUCER = 150; const int TOTAL = PRODUCERS * PER_PRODUCER; std::atomic counter{0}; std::atomic producers_done{false}; std::atomic resize_calls{0}; std::thread resizer([&tp, &producers_done, &resize_calls]() { int i = 0; while(!producers_done.load()) { std::size_t count = ((i % 2) == 0 ? 2 : 6); int rv = tp.set_threads(count); if(rv == 0) resize_calls.fetch_add(1); ++i; } }); std::vector producers; for(int p = 0; p < PRODUCERS; ++p) { producers.emplace_back([&tp, &counter]() { auto ptok = tp.ptoken(); for(int i = 0; i < PER_PRODUCER; ++i) tp.enqueue_work(ptok, [&counter](){ counter.fetch_add(1); }); }); } for(auto &t : producers) t.join(); producers_done.store(true); resizer.join(); TEST_CHECK(resize_calls.load() > 0); TEST_CHECK(wait_until([&counter, TOTAL](){ return counter.load() == TOTAL; }, 10000)); TEST_CHECK(counter.load() == TOTAL); } void test_tp_repeated_resize_stress() { ThreadPool tp(2, 64, "test.resize"); const int ROUNDS = 30; const int PER_ROUND = 20; const int TOTAL = ROUNDS * PER_ROUND; std::atomic counter{0}; for(int r = 0; r < ROUNDS; ++r) { std::size_t target = 1 + (r % 5); int rv = tp.set_threads(target); TEST_CHECK(rv == 0); for(int i = 0; i < PER_ROUND; ++i) tp.enqueue_work([&counter](){ counter.fetch_add(1); }); } TEST_CHECK(wait_until([&counter, TOTAL](){ return counter.load() == TOTAL; }, 10000)); TEST_CHECK(counter.load() == TOTAL); } void test_tp_many_producers_many_tasks_stress() { ThreadPool tp(8, 256, "test.mpmt"); const int PRODUCERS = 8; const int ITEMS_PER = 200; const int TOTAL = PRODUCERS * ITEMS_PER; std::atomic counter{0}; std::vector producers; for(int p = 0; p < PRODUCERS; ++p) { producers.emplace_back([&tp, &counter]() { auto ptok = tp.ptoken(); for(int i = 0; i < ITEMS_PER; ++i) tp.enqueue_work(ptok, [&counter](){ counter.fetch_add(1); }); }); } for(auto &t : producers) t.join(); TEST_CHECK(wait_until([&counter, TOTAL](){ return counter.load() == TOTAL; }, 10000)); TEST_CHECK(counter.load() == TOTAL); } void test_tp_heavy_mixed_resize_and_enqueue_stress() { ThreadPool tp(6, 512, "test.hmix"); const int PRODUCERS = 10; const int PER_PRODUCER = 2500; const int TOTAL = PRODUCERS * PER_PRODUCER; std::atomic executed{0}; std::atomic producers_done{false}; std::atomic resize_ops{0}; std::thread resizer([&tp, &producers_done, &resize_ops]() { int i = 0; while(!producers_done.load()) { const std::size_t target = 2 + (i % 10); int rv = tp.set_threads(target); if(rv == 0) resize_ops.fetch_add(1); ++i; } }); std::vector producers; producers.reserve(PRODUCERS); for(int p = 0; p < PRODUCERS; ++p) { producers.emplace_back([&tp, &executed]() { auto ptok = tp.ptoken(); for(int i = 0; i < PER_PRODUCER; ++i) tp.enqueue_work(ptok, [&executed](){ executed.fetch_add(1); }); }); } for(auto &t : producers) t.join(); producers_done.store(true); resizer.join(); TEST_CHECK(resize_ops.load() > 0); TEST_CHECK(wait_until([&executed, TOTAL](){ return executed.load() == TOTAL; }, 30000)); TEST_CHECK(executed.load() == TOTAL); } void test_tp_heavy_try_enqueue_pressure() { ThreadPool tp(1, 1, "test.hpress"); const int PRODUCERS = 6; const int ATTEMPTS_PER = 2500; std::atomic accepted{0}; std::atomic rejected{0}; std::atomic executed{0}; std::vector producers; producers.reserve(PRODUCERS); for(int p = 0; p < PRODUCERS; ++p) { producers.emplace_back([&tp, &accepted, &rejected, &executed]() { auto ptok = tp.ptoken(); for(int i = 0; i < ATTEMPTS_PER; ++i) { bool ok = tp.try_enqueue_work_for(ptok, 200, [&executed]() { std::this_thread::sleep_for(std::chrono::microseconds(50)); executed.fetch_add(1); }); if(ok) accepted.fetch_add(1); else rejected.fetch_add(1); } }); } for(auto &t : producers) t.join(); TEST_CHECK(accepted.load() > 0); TEST_CHECK(rejected.load() > 0); TEST_CHECK(wait_until([&executed, &accepted]() { return executed.load() == accepted.load(); }, 30000)); TEST_CHECK(executed.load() == accepted.load()); } void test_tp_heavy_repeated_construct_destroy() { const int ROUNDS = 150; for(int r = 0; r < ROUNDS; ++r) { std::atomic counter{0}; { ThreadPool tp(4, 128, "test.hctor"); for(int i = 0; i < 400; ++i) tp.enqueue_work([&counter]() { std::this_thread::sleep_for(std::chrono::microseconds(25)); counter.fetch_add(1); }); } TEST_CHECK(counter.load() == 400); } } void test_tp_heavy_enqueue_task_mixed_outcomes() { ThreadPool tp(8, 512, "test.htask"); const int N = 12000; std::vector> futures; futures.reserve(N); for(int i = 0; i < N; ++i) { futures.emplace_back(tp.enqueue_task([i]() -> int { if((i % 11) == 0) throw std::runtime_error("heavy task error"); return i; })); } long long expected_sum = 0; int expected_success = 0; int expected_errors = 0; for(int i = 0; i < N; ++i) { if((i % 11) == 0) { expected_errors += 1; } else { expected_success += 1; expected_sum += i; } } int actual_success = 0; int actual_errors = 0; long long actual_sum = 0; for(auto &f : futures) { try { actual_sum += f.get(); actual_success += 1; } catch(const std::runtime_error &e) { actual_errors += 1; TEST_CHECK(std::string(e.what()) == "heavy task error"); } } TEST_CHECK(actual_success == expected_success); TEST_CHECK(actual_errors == expected_errors); TEST_CHECK(actual_sum == expected_sum); } void test_tp_heavy_add_remove_churn_under_enqueue() { ThreadPool tp(6, 512, "test.hchurn"); const int PRODUCERS = 6; const int PER_PRODUCER = 2200; const int TOTAL = PRODUCERS * PER_PRODUCER; const int CHURN_OPS = 700; std::atomic executed{0}; std::atomic producers_done{false}; std::atomic add_ok{0}; std::atomic remove_ok{0}; std::thread churner([&tp, &producers_done, &add_ok, &remove_ok]() { int i = 0; while(!producers_done.load() && i < CHURN_OPS) { int rv_add = tp.add_thread(); if(rv_add == 0) add_ok.fetch_add(1); int rv_remove = tp.remove_thread(); if(rv_remove == 0) remove_ok.fetch_add(1); ++i; } }); std::vector producers; producers.reserve(PRODUCERS); for(int p = 0; p < PRODUCERS; ++p) { producers.emplace_back([&tp, &executed]() { auto ptok = tp.ptoken(); for(int i = 0; i < PER_PRODUCER; ++i) tp.enqueue_work(ptok, [&executed]() { executed.fetch_add(1); }); }); } for(auto &t : producers) t.join(); producers_done.store(true); churner.join(); TEST_CHECK(add_ok.load() > 0); TEST_CHECK(remove_ok.load() > 0); TEST_CHECK(wait_until([&executed, TOTAL](){ return executed.load() == TOTAL; }, 30000)); TEST_CHECK(executed.load() == TOTAL); } void test_config_has_key() { Config cfg; TEST_CHECK(cfg.has_key("async-read") == true); TEST_CHECK(cfg.has_key("branches") == true); TEST_CHECK(cfg.has_key("version") == true); TEST_CHECK(cfg.has_key("nonexistent-key-xyz") == false); TEST_CHECK(cfg.has_key("") == false); // Underscore form should NOT match (has_key does no normalization) TEST_CHECK(cfg.has_key("async_read") == false); } void test_config_keys() { Config cfg; const auto &map = cfg.get_map(); // Result must be non-empty TEST_CHECK(!map.empty()); // Verify known keys are present TEST_CHECK(map.count("async-read") > 0); TEST_CHECK(map.count("branches") > 0); TEST_CHECK(map.count("version") > 0); // No key should be empty for(const auto &[k, v] : map) TEST_CHECK(!k.empty()); } void test_config_keys_xattr() { Config cfg; ssize_t needed = cfg.keys_listxattr(nullptr, 0); TEST_CHECK(needed > 0); std::string s(needed, '\0'); ssize_t written = cfg.keys_listxattr(s.data(), needed); TEST_CHECK(written > 0); s.resize(written); // Every token must start with "user.mergerfs." and end with NUL const char *p = s.data(); const char *end = p + s.size(); int count = 0; while(p < end) { const char *nul = static_cast(memchr(p, '\0', end - p)); TEST_CHECK(nul != nullptr); if(!nul) break; std::string entry(p, nul); TEST_CHECK(str::startswith(entry, "user.mergerfs.")); ++count; p = nul + 1; } TEST_CHECK(count > 0); } void test_config_keys_listxattr_size() { Config cfg; ssize_t sz = cfg.keys_listxattr_size(); TEST_CHECK(sz > 0); // Size must accommodate a buffer filled by keys_listxattr std::string buf(sz, '\0'); ssize_t written = cfg.keys_listxattr(buf.data(), sz); TEST_CHECK(written > 0); TEST_CHECK(written <= sz); } void test_config_keys_listxattr_fill() { Config cfg; // size=0 must return required size (not write anything) ssize_t needed = cfg.keys_listxattr(nullptr, 0); TEST_CHECK(needed > 0); // Allocate exact size and fill std::vector buf(needed); ssize_t written = cfg.keys_listxattr(buf.data(), needed); TEST_CHECK(written > 0); TEST_CHECK(written <= needed); // Every entry must start with "user.mergerfs." and end with NUL const char *p = buf.data(); const char *end = p + written; int count = 0; while(p < end) { const char *nul = static_cast(memchr(p, '\0', end - p)); TEST_CHECK(nul != nullptr); if(!nul) break; std::string entry(p, nul); TEST_CHECK(str::startswith(entry, "user.mergerfs.")); TEST_CHECK(entry.size() > strlen("user.mergerfs.")); ++count; p = nul + 1; } TEST_CHECK(count > 0); // Keys reported by keys_listxattr must be consistent across two calls std::vector xattr_buf(needed); ssize_t xattr_written = cfg.keys_listxattr(xattr_buf.data(), needed); TEST_CHECK(xattr_written == written); TEST_CHECK(memcmp(buf.data(), xattr_buf.data(), written) == 0); } void test_config_keys_listxattr_erange() { Config cfg; // Buffer too small (1 byte) must return -ERANGE char tiny[1] = {'\xff'}; ssize_t rv = cfg.keys_listxattr(tiny, 1); TEST_CHECK(rv == -ERANGE); } void test_config_get_set_roundtrip() { Config cfg; std::string val; // bool: async-read TEST_CHECK(cfg.set("async-read", "false") == 0); TEST_CHECK(cfg.get("async-read", &val) == 0); TEST_CHECK(val == "false"); TEST_CHECK(cfg.set("async-read", "true") == 0); TEST_CHECK(cfg.get("async-read", &val) == 0); TEST_CHECK(val == "true"); // u64: cache-attr TEST_CHECK(cfg.set("cache.attr", "42") == 0); TEST_CHECK(cfg.get("cache.attr", &val) == 0); TEST_CHECK(val == "42"); // string: fsname TEST_CHECK(cfg.set("fsname", "myfs") == 0); TEST_CHECK(cfg.get("fsname", &val) == 0); TEST_CHECK(val == "myfs"); } void test_config_get_unknown_key() { Config cfg; std::string val; int rv = cfg.get("no-such-key", &val); TEST_CHECK(rv == -ENOATTR); } void test_config_set_unknown_key() { Config cfg; int rv = cfg.set("no-such-key", "value"); TEST_CHECK(rv == -ENOATTR); } void test_config_set_invalid_value() { Config cfg; std::string val; // "async-read" is a bool — "notabool" must be rejected int rv = cfg.set("async-read", "notabool"); TEST_CHECK(rv == -EINVAL); // value must be unchanged TEST_CHECK(cfg.get("async-read", &val) == 0); TEST_CHECK(val == "true"); } void test_config_set_underscore_normalization() { Config cfg; std::string val; // Underscores must be converted to hyphens TEST_CHECK(cfg.set("async_read", "false") == 0); TEST_CHECK(cfg.get("async-read", &val) == 0); TEST_CHECK(val == "false"); } void test_config_get_underscore_normalization() { Config cfg; std::string val; TEST_CHECK(cfg.set("async-read", "false") == 0); TEST_CHECK(cfg.get("async_read", &val) == 0); TEST_CHECK(val == "false"); } void test_config_set_kv_form() { Config cfg; std::string val; TEST_CHECK(cfg.set("async-read=false") == 0); TEST_CHECK(cfg.get("async-read", &val) == 0); TEST_CHECK(val == "false"); // With spaces around delimiter TEST_CHECK(cfg.set("async-read = true") == 0); TEST_CHECK(cfg.get("async-read", &val) == 0); TEST_CHECK(val == "true"); } void test_config_readonly_before_init() { Config cfg; std::string val; // version is RO even before finish_initializing() int rv = cfg.set("version", "9.9.9"); TEST_CHECK(rv == -EROFS); } void test_config_readonly_after_init() { Config cfg; std::string val; // Before init: mutable fields can be set TEST_CHECK(cfg.set("async-read", "false") == 0); cfg.finish_initializing(); // async-read is marked RO after init int rv = cfg.set("async-read", "true"); TEST_CHECK(rv == -EROFS); // Value must be unchanged TEST_CHECK(cfg.get("async-read", &val) == 0); TEST_CHECK(val == "false"); } void test_config_mutable_after_init() { Config cfg; std::string val; cfg.finish_initializing(); // dropcacheonclose is NOT RO after init — should still be settable TEST_CHECK(cfg.set("dropcacheonclose", "true") == 0); TEST_CHECK(cfg.get("dropcacheonclose", &val) == 0); TEST_CHECK(val == "true"); } void test_config_from_stream_basic() { Config cfg; std::string val; std::istringstream ss("async-read=false\ncache.attr=99\n"); int rv = cfg.from_stream(ss); TEST_CHECK(rv == 0); TEST_CHECK(cfg.get("async-read", &val) == 0); TEST_CHECK(val == "false"); TEST_CHECK(cfg.get("cache.attr", &val) == 0); TEST_CHECK(val == "99"); } void test_config_from_stream_comments_and_blanks() { Config cfg; std::string val; std::istringstream ss( "# this is a comment\n" "\n" " \n" "async-read=false\n" "# another comment\n" "cache.attr=7\n" ); int rv = cfg.from_stream(ss); TEST_CHECK(rv == 0); TEST_CHECK(cfg.get("async-read", &val) == 0); TEST_CHECK(val == "false"); TEST_CHECK(cfg.get("cache.attr", &val) == 0); TEST_CHECK(val == "7"); } void test_config_from_stream_bad_lines() { Config cfg; std::istringstream ss("async-read=false\nbad-key-xyz=oops\ncache.attr=5\n"); int rv = cfg.from_stream(ss); // Must report error but still apply valid lines TEST_CHECK(rv == -EINVAL); TEST_CHECK(!cfg.errs.empty()); std::string val; TEST_CHECK(cfg.get("async-read", &val) == 0); TEST_CHECK(val == "false"); TEST_CHECK(cfg.get("cache.attr", &val) == 0); TEST_CHECK(val == "5"); } void test_config_from_file_nonexistent() { Config cfg; int rv = cfg.from_file("/this/path/does/not/exist/config.txt"); TEST_CHECK(rv < 0); TEST_CHECK(!cfg.errs.empty()); } void test_config_is_rootdir() { TEST_CHECK(Config::is_rootdir("") == true); TEST_CHECK(Config::is_rootdir("/") == false); TEST_CHECK(Config::is_rootdir("/foo") == false); } void test_config_is_ctrl_file() { TEST_CHECK(Config::is_ctrl_file(".mergerfs") == true); TEST_CHECK(Config::is_ctrl_file("/.mergerfs") == false); TEST_CHECK(Config::is_ctrl_file("mergerfs") == false); TEST_CHECK(Config::is_ctrl_file("") == false); } void test_config_is_mergerfs_xattr() { TEST_CHECK(Config::is_mergerfs_xattr("user.mergerfs.async-read") == true); TEST_CHECK(Config::is_mergerfs_xattr("user.mergerfs.") == true); TEST_CHECK(Config::is_mergerfs_xattr("user.mergerfs") == false); TEST_CHECK(Config::is_mergerfs_xattr("user.other.async-read") == false); TEST_CHECK(Config::is_mergerfs_xattr("") == false); } void test_config_is_cmd_xattr() { TEST_CHECK(Config::is_cmd_xattr("user.mergerfs.cmd.foo") == true); TEST_CHECK(Config::is_cmd_xattr("user.mergerfs.cmd.") == true); TEST_CHECK(Config::is_cmd_xattr("user.mergerfs.async-read") == false); TEST_CHECK(Config::is_cmd_xattr("user.mergerfs.cmd") == false); TEST_CHECK(Config::is_cmd_xattr("") == false); } void test_config_prune_ctrl_xattr() { TEST_CHECK(Config::prune_ctrl_xattr("user.mergerfs.async-read") == "async-read"); TEST_CHECK(Config::prune_ctrl_xattr("user.mergerfs.cache.attr") == "cache.attr"); // Exactly the prefix — nothing after it: returns empty TEST_CHECK(Config::prune_ctrl_xattr("user.mergerfs.") == ""); // Shorter than prefix — returns empty TEST_CHECK(Config::prune_ctrl_xattr("user.mergerfs") == ""); TEST_CHECK(Config::prune_ctrl_xattr("") == ""); } void test_config_prune_cmd_xattr() { using sv = std::string_view; TEST_CHECK(Config::prune_cmd_xattr("user.mergerfs.cmd.foo") == sv("foo")); TEST_CHECK(Config::prune_cmd_xattr("user.mergerfs.cmd.bar.baz") == sv("bar.baz")); TEST_CHECK(Config::prune_cmd_xattr("user.mergerfs.cmd.") == sv("")); TEST_CHECK(Config::prune_cmd_xattr("user.mergerfs.cmd") == sv("")); TEST_CHECK(Config::prune_cmd_xattr("") == sv("")); } void test_fs_copyfile_basic() { int rv; int src_fd; struct stat st; fs::path src_path; fs::path dst_path; fs::path tmp_dir; char tmp_template[] = "/tmp/mergerfs-test-copyfile-basic-XXXXXX"; if(::mkdtemp(tmp_template) == nullptr) { TEST_CHECK(false); return; } tmp_dir = tmp_template; src_path = tmp_dir / "src.bin"; dst_path = tmp_dir / "dst.bin"; src_fd = ::open(src_path.c_str(),O_RDWR|O_CREAT|O_TRUNC,0600); TEST_CHECK(src_fd >= 0); if(src_fd < 0) { std::filesystem::remove_all(tmp_dir); return; } rv = ::ftruncate(src_fd,(16 * 1024 * 1024)); TEST_CHECK(rv == 0); rv = ::pwrite(src_fd,"A",1,0); TEST_CHECK(rv == 1); rv = ::pwrite(src_fd,"Z",1,((16 * 1024 * 1024) - 1)); TEST_CHECK(rv == 1); rv = fs::copyfile(src_fd,dst_path,{.cleanup_failure = true}); TEST_CHECK(rv == 0); rv = ::fstat(src_fd,&st); TEST_CHECK(rv == 0); struct stat dst_st; rv = ::stat(dst_path.c_str(),&dst_st); TEST_CHECK(rv == 0); if(rv == 0) TEST_CHECK(st.st_size == dst_st.st_size); char c; int dst_fd = ::open(dst_path.c_str(),O_RDONLY); TEST_CHECK(dst_fd >= 0); if(dst_fd >= 0) { rv = ::pread(dst_fd,&c,1,0); TEST_CHECK((rv == 1) && (c == 'A')); rv = ::pread(dst_fd,&c,1,(dst_st.st_size - 1)); TEST_CHECK((rv == 1) && (c == 'Z')); ::close(dst_fd); } ::close(src_fd); std::filesystem::remove_all(tmp_dir); } void test_fs_copyfile_source_changes_cleanup_tmpfiles() { int rv; int src_fd; fs::path src_path; fs::path dst_path; fs::path tmp_dir; std::atomic stop_mutator{false}; std::atomic mutator_updates{0}; char tmp_template[] = "/tmp/mergerfs-test-copyfile-race-XXXXXX"; if(::mkdtemp(tmp_template) == nullptr) { TEST_CHECK(false); return; } tmp_dir = tmp_template; src_path = tmp_dir / "src.bin"; dst_path = tmp_dir / "dst.bin"; src_fd = ::open(src_path.c_str(),O_RDWR|O_CREAT|O_TRUNC,0600); TEST_CHECK(src_fd >= 0); if(src_fd < 0) { std::filesystem::remove_all(tmp_dir); return; } rv = ::ftruncate(src_fd,(64 * 1024 * 1024)); TEST_CHECK(rv == 0); auto mutator = std::thread([&]() { for(int i = 0; i < 200 && !stop_mutator.load(); ++i) { struct timespec ts[2]; ::clock_gettime(CLOCK_REALTIME,&ts[0]); ts[1] = ts[0]; ::utimensat(AT_FDCWD,src_path.c_str(),ts,0); mutator_updates.fetch_add(1); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } }); rv = fs::copyfile(src_fd,dst_path,{.cleanup_failure = true}); stop_mutator.store(true); mutator.join(); TEST_CHECK(rv == 0); TEST_CHECK(mutator_updates.load() > 0); bool found_tmp_file = false; std::string tmp_prefix; tmp_prefix = "."; tmp_prefix += dst_path.filename().string(); tmp_prefix += "_"; for(const auto &entry : std::filesystem::directory_iterator(tmp_dir)) { const std::string name = entry.path().filename().string(); if(name.rfind(tmp_prefix,0) == 0) { found_tmp_file = true; break; } } TEST_CHECK(found_tmp_file == false); ::close(src_fd); std::filesystem::remove_all(tmp_dir); } TEST_LIST = { {"nop",test_nop}, {"config_bool",test_config_bool}, {"config_uint64",test_config_uint64}, {"config_int",test_config_int}, {"config_str",test_config_str}, {"config_branches",test_config_branches}, {"branch_default_ctor_mode",test_branch_default_ctor_mode}, {"branch_to_string_modes",test_branch_to_string_modes}, {"branch_to_string_with_minfreespace",test_branch_to_string_with_minfreespace}, {"branch_mode_predicates",test_branch_mode_predicates}, {"branch_minfreespace_pointer_vs_value",test_branch_minfreespace_pointer_vs_value}, {"branch_copy_constructor",test_branch_copy_constructor}, {"branch_copy_assignment",test_branch_copy_assignment}, {"branches_default_minfreespace",test_branches_default_minfreespace}, {"branches_per_branch_minfreespace_overrides_default",test_branches_per_branch_minfreespace_overrides_default}, {"branches_add_end_operator",test_branches_add_end_operator}, {"branches_add_begin_operator",test_branches_add_begin_operator}, {"branches_set_operator",test_branches_set_operator}, {"branches_erase_end_operator",test_branches_erase_end_operator}, {"branches_erase_begin_operator",test_branches_erase_begin_operator}, {"branches_erase_fnmatch_operator",test_branches_erase_fnmatch_operator}, {"branches_erase_fnmatch_multiple_patterns",test_branches_erase_fnmatch_multiple_patterns}, {"branches_invalid_mode",test_branches_invalid_mode}, {"branches_invalid_minfreespace",test_branches_invalid_minfreespace}, {"branches_to_paths",test_branches_to_paths}, {"branches_cow_semantics",test_branches_cow_semantics}, {"branches_mode_round_trip",test_branches_mode_round_trip}, {"branches_srcmounts_to_string",test_branches_srcmounts_to_string}, {"branches_srcmounts_empty",test_branches_srcmounts_empty}, {"branch_to_string_1t",test_branch_to_string_1t}, {"branch_to_string_non_exact_multiple",test_branch_to_string_non_exact_multiple}, {"branch_to_string_zero_owned_minfreespace",test_branch_to_string_zero_owned_minfreespace}, {"branch_copy_pointer_aliasing",test_branch_copy_pointer_aliasing}, {"branches_mode_case_sensitivity",test_branches_mode_case_sensitivity}, {"branches_minfreespace_lowercase_suffixes",test_branches_minfreespace_lowercase_suffixes}, {"branches_minfreespace_bytes_suffix",test_branches_minfreespace_bytes_suffix}, {"branches_minfreespace_zero",test_branches_minfreespace_zero}, {"branches_minfreespace_negative_invalid",test_branches_minfreespace_negative_invalid}, {"branches_minfreespace_overflow",test_branches_minfreespace_overflow}, {"branches_minfreespace_round_trip",test_branches_minfreespace_round_trip}, {"branches_minfreespace_round_trip_1t",test_branches_minfreespace_round_trip_1t}, {"branches_clear_with_equals_alone",test_branches_clear_with_equals_alone}, {"branches_empty_string_clears_same_as_equals",test_branches_empty_string_clears_same_as_equals}, {"branches_plus_and_plus_greater_equivalent",test_branches_plus_and_plus_greater_equivalent}, {"branches_unknown_instruction_einval",test_branches_unknown_instruction_einval}, {"branches_empty_options_after_equals_einval",test_branches_empty_options_after_equals_einval}, {"branches_path_with_equals_in_name",test_branches_path_with_equals_in_name}, {"branches_add_end_multiple_paths",test_branches_add_end_multiple_paths}, {"branches_add_begin_multiple_paths",test_branches_add_begin_multiple_paths}, {"branches_erase_empty_pattern_noop",test_branches_erase_empty_pattern_noop}, {"branches_erase_wildcard_removes_all",test_branches_erase_wildcard_removes_all}, {"branches_erase_nonmatching_pattern_noop",test_branches_erase_nonmatching_pattern_noop}, {"branches_erase_multi_pattern_first_miss_second_hit",test_branches_erase_multi_pattern_first_miss_second_hit}, {"branches_erase_end_single_element",test_branches_erase_end_single_element}, {"branches_erase_begin_single_element",test_branches_erase_begin_single_element}, {"branches_erase_end_empty_returns_enoent",test_branches_erase_end_empty_returns_enoent}, {"branches_erase_begin_empty_returns_enoent",test_branches_erase_begin_empty_returns_enoent}, {"branches_clear_then_add",test_branches_clear_then_add}, {"branches_add_begin_then_end_then_erase_both",test_branches_add_begin_then_end_then_erase_both}, {"branches_error_in_multipath_add_leaves_list_unchanged",test_branches_error_in_multipath_add_leaves_list_unchanged}, {"branches_error_in_set_leaves_list_unchanged",test_branches_error_in_set_leaves_list_unchanged}, {"branches_global_minfreespace_live_tracking",test_branches_global_minfreespace_live_tracking}, {"branches_srcmounts_from_string_noop",test_branches_srcmounts_from_string_noop}, {"branches_srcmounts_reflects_mutations",test_branches_srcmounts_reflects_mutations}, {"config_cachefiles",test_config_cachefiles}, {"config_inodecalc",test_config_inodecalc}, {"config_moveonenospc",test_config_moveonenospc}, {"config_nfsopenhack",test_config_nfsopenhack}, {"config_readdir",test_config_readdir}, {"config_statfs",test_config_statfs}, {"config_statfsignore",test_config_statfs_ignore}, {"config_xattr",test_config_xattr}, {"config",test_config}, {"config_has_key",test_config_has_key}, {"config_keys",test_config_keys}, {"config_keys_xattr",test_config_keys_xattr}, {"config_keys_listxattr_size",test_config_keys_listxattr_size}, {"config_keys_listxattr_fill",test_config_keys_listxattr_fill}, {"config_keys_listxattr_erange",test_config_keys_listxattr_erange}, {"config_get_set_roundtrip",test_config_get_set_roundtrip}, {"config_get_unknown_key",test_config_get_unknown_key}, {"config_set_unknown_key",test_config_set_unknown_key}, {"config_set_invalid_value",test_config_set_invalid_value}, {"config_set_underscore_normalization",test_config_set_underscore_normalization}, {"config_get_underscore_normalization",test_config_get_underscore_normalization}, {"config_set_kv_form",test_config_set_kv_form}, {"config_readonly_before_init",test_config_readonly_before_init}, {"config_readonly_after_init",test_config_readonly_after_init}, {"config_mutable_after_init",test_config_mutable_after_init}, {"config_from_stream_basic",test_config_from_stream_basic}, {"config_from_stream_comments_and_blanks",test_config_from_stream_comments_and_blanks}, {"config_from_stream_bad_lines",test_config_from_stream_bad_lines}, {"config_from_file_nonexistent",test_config_from_file_nonexistent}, {"config_is_rootdir",test_config_is_rootdir}, {"config_is_ctrl_file",test_config_is_ctrl_file}, {"config_is_mergerfs_xattr",test_config_is_mergerfs_xattr}, {"config_is_cmd_xattr",test_config_is_cmd_xattr}, {"config_prune_ctrl_xattr",test_config_prune_ctrl_xattr}, {"config_prune_cmd_xattr",test_config_prune_cmd_xattr}, {"fs_copyfile_basic",test_fs_copyfile_basic}, {"fs_copyfile_source_changes_cleanup_tmpfiles",test_fs_copyfile_source_changes_cleanup_tmpfiles}, {"str",test_str_stuff}, {"tp_construct_default",test_tp_construct_default}, {"tp_construct_named",test_tp_construct_named}, {"tp_construct_zero_threads_throws",test_tp_construct_zero_threads_throws}, {"tp_enqueue_work",test_tp_enqueue_work}, {"tp_enqueue_work_with_ptoken",test_tp_enqueue_work_with_ptoken}, {"tp_try_enqueue_work",test_tp_try_enqueue_work}, {"tp_try_enqueue_work_with_ptoken",test_tp_try_enqueue_work_with_ptoken}, {"tp_try_enqueue_work_for",test_tp_try_enqueue_work_for}, {"tp_try_enqueue_work_for_with_ptoken",test_tp_try_enqueue_work_for_with_ptoken}, {"tp_enqueue_task_int",test_tp_enqueue_task_int}, {"tp_enqueue_task_void",test_tp_enqueue_task_void}, {"tp_enqueue_task_string",test_tp_enqueue_task_string}, {"tp_enqueue_task_exception",test_tp_enqueue_task_exception}, {"tp_enqueue_task_multiple",test_tp_enqueue_task_multiple}, {"tp_threads_returns_correct_count",test_tp_threads_returns_correct_count}, {"tp_threads_unique_ids",test_tp_threads_unique_ids}, {"tp_add_thread",test_tp_add_thread}, {"tp_remove_thread",test_tp_remove_thread}, {"tp_remove_thread_refuses_last",test_tp_remove_thread_refuses_last}, {"tp_set_threads_grow",test_tp_set_threads_grow}, {"tp_set_threads_shrink",test_tp_set_threads_shrink}, {"tp_set_threads_zero",test_tp_set_threads_zero}, {"tp_set_threads_same",test_tp_set_threads_same}, {"tp_work_after_add_thread",test_tp_work_after_add_thread}, {"tp_work_after_remove_thread",test_tp_work_after_remove_thread}, {"tp_worker_exception_no_crash",test_tp_worker_exception_no_crash}, {"tp_concurrent_enqueue",test_tp_concurrent_enqueue}, {"tp_ptoken_creation",test_tp_ptoken_creation}, {"tp_destruction_drains_queue",test_tp_destruction_drains_queue}, {"tp_move_only_callable",test_tp_move_only_callable}, {"tp_work_ordering_single_thread",test_tp_work_ordering_single_thread}, {"tp_try_enqueue_work_fails_when_full",test_tp_try_enqueue_work_fails_when_full}, {"tp_try_enqueue_work_for_times_out_when_full",test_tp_try_enqueue_work_for_times_out_when_full}, {"tp_enqueue_work_blocks_until_slot_available",test_tp_enqueue_work_blocks_until_slot_available}, {"tp_worker_nonstd_exception_no_crash",test_tp_worker_nonstd_exception_no_crash}, {"tp_remove_thread_under_load",test_tp_remove_thread_under_load}, {"tp_set_threads_concurrent_with_enqueue",test_tp_set_threads_concurrent_with_enqueue}, {"tp_repeated_resize_stress",test_tp_repeated_resize_stress}, {"tp_many_producers_many_tasks_stress",test_tp_many_producers_many_tasks_stress}, {"tp_heavy_mixed_resize_and_enqueue_stress",test_tp_heavy_mixed_resize_and_enqueue_stress}, {"tp_heavy_try_enqueue_pressure",test_tp_heavy_try_enqueue_pressure}, {"tp_heavy_repeated_construct_destroy",test_tp_heavy_repeated_construct_destroy}, {"tp_heavy_enqueue_task_mixed_outcomes",test_tp_heavy_enqueue_task_mixed_outcomes}, {"tp_heavy_add_remove_churn_under_enqueue",test_tp_heavy_add_remove_churn_under_enqueue}, {NULL,NULL} };