mirror of
https://github.com/trapexit/mergerfs.git
synced 2026-10-19 16:16:48 +00:00
std::filesystem::path is wrong for mergerfs's hot path:
- `branch->path / fusepath_` heap-allocates per FUSE op (3 allocs each
on libstdc++; the SSO buffer is 15 bytes and any joined path exceeds it).
- The implicit `operator string_type()` adds another std::string when
passing to `fs::*` wrappers that take `const std::string&`.
- Per-branch loops rebuild fullpath from scratch every iteration.
The new fs::relpath is a fixed 8192-byte (2 * PATH_MAX) inline buffer,
NUL-terminated, never heap-allocates. The rel data is anchored at offset
REL_OFFSET = 4096; the 4096 bytes before the anchor form the "prefix
area" so a long base/prefix can be written there in place via
set_prefix() without disturbing the rel. The remaining ~4096 bytes
after the rel are available for trailing append (filenames in
readdir-style loops, etc.).
set_prefix's cost is O(prefix size) -- only the actually-used bytes
are touched -- so the buffer size is a stack-footprint question, not a
hot-path cost. FUSE worker threads use the pthread default stack
(~8 MB on Linux); a 4-relpath frame in fuse_link/fuse_rename is 32 KB,
~0.4% of the stack budget.
The per-branch hot loop:
fs::relpath fullpath = fusepath_; // one copy of the rel
for (auto branch : branches) {
fullpath.set_prefix(branch->path); // writes only [start, 4096)
fs::syscall(fullpath);
}
set_prefix only rewrites the prefix area; the rel stays anchored. A
cached _rel_size means no recomputation across iterations.
Microbenchmark (8 branches, 30-char fusepath, -O3, 4 M path builds):
variant ns/build allocs/build
-------------------------------------- -------- ------------
origin/master (std::filesystem::path) 158 3.00
fs::relpath assign_concat (single-shot) 9.1 0
fs::relpath set_prefix (loop-optimized) 4.5 0
35x faster, zero allocations in the hot path.
Contract: paths flow through fs::relpath in canonical form -- prefix
without trailing '/', rel without leading '/'. No sanitization;
violations yield double slashes. Mergerfs satisfies this by
construction:
- libfuse's try_get_path() walks leaf-to-root via prepend(), then
calls consolidate_to_rel(strip=true) which drops the leading '/'
that prepend produces.
- branch paths are realpath-canonicalized at config load so they
never have a trailing slash.
Mergerfs has exactly one path-manipulation type: a relative fusepath.
Branch paths and other absolute paths are stored as std::string -- they
are spliced into the prefix area of an fs::relpath at syscall time but
are never themselves mutated as path objects.
Major changes:
- vendored/libfuse/{include/fs_path.hpp,lib/fs_path.cpp}: new
fs::relpath class (8 KB inline, REL anchor at 4096, set_prefix /
clear_prefix / consolidate_to_rel, _rel_size cache, self-aliasing-
safe assign_concat, sticky ENAMETOOLONG via errnum()).
- 30+ FUSE-op call sites converted from `fullpath = branch / fusepath`
to the `set_prefix` loop pattern (fuse_unlink, fuse_chmod,
fuse_chown, fuse_listxattr, fuse_truncate, fuse_utimens,
fuse_setxattr, fuse_rmdir, fuse_removexattr, fuse_link's
preserve-path and create-path loops, fuse_rename's create-path
and preserve-path loops, fuse_statfs FULL mode, fs::findallfiles,
fs::findonfs).
- fs::* syscall wrappers that previously only took
`const std::string&` got `const char*` overloads so fs::relpath's
implicit `operator const char*()` lands cleanly (fs::link,
fs::lutimens, fs::umount_lazy, fs::open_dir_ro, fs::readlink,
fs::lremovexattr, fs::lstatvfs, fs::info, fs::statvfs_cache).
- fs::exists gained a (fs::relpath&, basepath, ...) overload that
calls set_prefix internally so per-branch policy fan-out loops
share a single fullpath, rewriting only the prefix area on each
iteration.
- All policies (epall, epff, eplfs, eplus, epmfs, eppfrd, ff, lup,
msplfs, msplus, mspmfs, msppfrd, newest) refactored to hoist the
fs::relpath fullpath construction out of the per-branch loop and
use the set_prefix-based fs::exists overload.
- fs::inode::ReaddirCalc::_fusepath migrated from std::string to
fs::relpath with the same resize/append API.
- std::filesystem removed entirely from the project: branches.cpp
and option_parser.cpp's exists/is_directory/equivalent calls now
use direct ::stat(); mergerfs_fsck.cpp's recursive_directory_iterator
was replaced with an opendir-based walk.
Tests (tests/tests.cpp): 27 new fs::relpath test cases covering
construction, set_prefix in/out of REL anchor, prefix()/rel()
accessors, clear_prefix, concat canonical form, self-aliasing
(`p /= "x"`), operator/=, resize zero-fill, swap, copy/move,
equality, implicit conversions, lexically_relative, remove_filename,
_rel_size invariant across every mutator, capacity bounds.
Full suite: 213/213 pass.
4805 lines
126 KiB
C++
4805 lines
126 KiB
C++
#include "acutest/acutest.h"
|
|
|
|
#include "config.hpp"
|
|
#include "fs_clonepath.hpp"
|
|
#include "fs_copyfile.hpp"
|
|
#include "fs_exists.hpp"
|
|
#include "fs_inode.hpp"
|
|
#include "fs_path.hpp"
|
|
#include "from_string.hpp"
|
|
#include "hashset.hpp"
|
|
#include "num.hpp"
|
|
#include "rapidhash/rapidhash.h"
|
|
#include "rnd.hpp"
|
|
#include "str.hpp"
|
|
#include "thread_pool.hpp"
|
|
|
|
#include <atomic>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fcntl.h>
|
|
#include <fstream>
|
|
#include <future>
|
|
#include <mutex>
|
|
#include <numeric>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <thread>
|
|
|
|
#include <errno.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
template<typename Predicate>
|
|
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<std::string> v;
|
|
std::set<std::string> s;
|
|
std::pair<std::string, std::string> 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<std::string> 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("+</tmp/b") == 0);
|
|
Branches::Ptr p = b;
|
|
TEST_CHECK((*p).size() == 2);
|
|
TEST_CHECK((*p)[0].path == "/tmp/b");
|
|
TEST_CHECK((*p)[1].path == "/tmp/a");
|
|
}
|
|
|
|
void
|
|
test_branches_set_operator()
|
|
{
|
|
Branches b;
|
|
|
|
TEST_CHECK(b.from_string("/tmp/a:/tmp/b") == 0);
|
|
TEST_CHECK(b->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<std::string> 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("+</tmp/b:/tmp/c") == 0);
|
|
Branches::Ptr p = b;
|
|
TEST_CHECK((*p).size() == 3);
|
|
TEST_CHECK((*p)[0].path == "/tmp/b");
|
|
TEST_CHECK((*p)[1].path == "/tmp/c");
|
|
TEST_CHECK((*p)[2].path == "/tmp/a");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// erase_fnmatch edge cases
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void
|
|
test_branches_erase_empty_pattern_noop()
|
|
{
|
|
Branches b;
|
|
|
|
TEST_CHECK(b.from_string("/tmp/a:/tmp/b") == 0);
|
|
|
|
// "-" alone (empty pattern) — no-op, list unchanged
|
|
TEST_CHECK(b.from_string("-") == 0);
|
|
TEST_CHECK(b->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/a") == 0); // prepend
|
|
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;
|
|
|
|
TEST_CHECK(b.from_string("/tmp/a") == 0);
|
|
|
|
// 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_fs_inode_readdir_calc_matches_calc()
|
|
{
|
|
const std::string branch_path("/mnt/disk1");
|
|
const std::vector<fs::relpath> dirpaths = {
|
|
fs::relpath(),
|
|
fs::relpath("tv/season_01"),
|
|
};
|
|
const std::vector<std::pair<std::string,mode_t>> entries = {
|
|
{"episode_01.mkv", S_IFREG},
|
|
{"extras", S_IFDIR},
|
|
{"nfo-file.nfo", S_IFREG},
|
|
{"a_very_long_entry_name_used_to_force_buffer_growth.txt", S_IFREG},
|
|
};
|
|
const std::vector<std::string> algos = {
|
|
"passthrough",
|
|
"path-hash",
|
|
"path-hash32",
|
|
"devino-hash",
|
|
"devino-hash32",
|
|
"hybrid-hash",
|
|
"hybrid-hash32",
|
|
};
|
|
|
|
for(const auto &algo : algos)
|
|
{
|
|
TEST_CHECK(fs::inode::set_algo(algo) == 0);
|
|
|
|
for(const auto &dirpath : dirpaths)
|
|
{
|
|
fs::inode::ReaddirCalc calc(branch_path,dirpath);
|
|
ino_t ino = 1000;
|
|
|
|
for(const auto &[name,mode] : entries)
|
|
{
|
|
const fs::relpath fullpath = dirpath / name;
|
|
const u64 expected = fs::inode::calc(branch_path,
|
|
fullpath,
|
|
mode,
|
|
ino);
|
|
const u64 actual = calc.calc(name.c_str(),
|
|
name.size(),
|
|
mode,
|
|
ino);
|
|
|
|
TEST_CHECK(actual == expected);
|
|
|
|
++ino;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
Config cfg;
|
|
|
|
TEST_CHECK(cfg.has_key("func.readdir") == true);
|
|
|
|
const auto &map = cfg.get_map();
|
|
TEST_CHECK(map.count("func.readdir") > 0);
|
|
}
|
|
|
|
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<int> 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<int> 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<int> 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<int> 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<int> 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<int> 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<bool> 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<int> 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<int> 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<bool> 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<int> counter{0};
|
|
const int PRODUCERS = 4;
|
|
const int ITEMS_PER = 50;
|
|
const int TOTAL = PRODUCERS * ITEMS_PER;
|
|
|
|
std::vector<std::thread> 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<std::future<int>> 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<int> 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<int> 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<int>(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<int> results;
|
|
std::mutex mtx;
|
|
const int N = 10;
|
|
|
|
for(int i = 0; i < N; ++i)
|
|
tp.enqueue_work([i, &results, &mtx](){
|
|
std::lock_guard<std::mutex> lk(mtx);
|
|
results.push_back(i);
|
|
});
|
|
|
|
// Wait for all work to complete
|
|
for(int i = 0; i < 2000; ++i)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lk(mtx);
|
|
if((int)results.size() == N)
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
|
|
std::lock_guard<std::mutex> 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<bool> release{false};
|
|
std::atomic<bool> worker_started{false};
|
|
std::atomic<int> 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<bool> release{false};
|
|
std::atomic<bool> worker_started{false};
|
|
std::atomic<int> 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<std::chrono::microseconds>(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<bool> release{false};
|
|
std::atomic<bool> worker_started{false};
|
|
std::atomic<bool> producer_returned{false};
|
|
std::atomic<int> 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<bool> 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<int> 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<bool> 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<int> counter{0};
|
|
|
|
std::atomic<bool> producers_done{false};
|
|
std::atomic<int> 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<std::thread> 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<int> 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<int> counter{0};
|
|
|
|
std::vector<std::thread> 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<int> executed{0};
|
|
std::atomic<bool> producers_done{false};
|
|
std::atomic<int> 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<std::thread> 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<int> accepted{0};
|
|
std::atomic<int> rejected{0};
|
|
std::atomic<int> executed{0};
|
|
|
|
std::vector<std::thread> 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<int> 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<std::future<int>> 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<int> executed{0};
|
|
std::atomic<bool> producers_done{false};
|
|
std::atomic<int> add_ok{0};
|
|
std::atomic<int> 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<std::thread> 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<const char*>(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<char> 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<const char*>(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<char> 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_remember_nodes_bool_numeric()
|
|
{
|
|
bool old_remember_nodes = fuse_cfg.remember_nodes;
|
|
Config cfg;
|
|
std::string val;
|
|
|
|
fuse_cfg.remember_nodes = false;
|
|
|
|
// remember-nodes accepts ints: 0 disables, non-zero enables
|
|
TEST_CHECK(cfg.set("remember-nodes", "0") == 0);
|
|
TEST_CHECK(cfg.get("remember-nodes", &val) == 0);
|
|
TEST_CHECK(val == "false");
|
|
|
|
TEST_CHECK(cfg.set("remember-nodes", "86400") == 0);
|
|
TEST_CHECK(cfg.get("remember-nodes", &val) == 0);
|
|
TEST_CHECK(val == "true");
|
|
|
|
TEST_CHECK(cfg.set("remember", "-1") == 0);
|
|
TEST_CHECK(cfg.get("remember", &val) == 0);
|
|
TEST_CHECK(val == "true");
|
|
|
|
// remember-nodes also accepts bool strings
|
|
TEST_CHECK(cfg.set("remember-nodes", "false") == 0);
|
|
TEST_CHECK(cfg.get("remember-nodes", &val) == 0);
|
|
TEST_CHECK(val == "false");
|
|
|
|
// never-forget-nodes is bool; it reflects the feature state
|
|
TEST_CHECK(cfg.set("never-forget-nodes", "true") == 0);
|
|
TEST_CHECK(cfg.get("never-forget-nodes", &val) == 0);
|
|
TEST_CHECK(val == "true");
|
|
|
|
// Querying remember-nodes after never-forget-nodes=true returns true
|
|
// (both alias the same fuse_cfg.remember_nodes)
|
|
TEST_CHECK(cfg.get("remember-nodes", &val) == 0);
|
|
TEST_CHECK(val == "true");
|
|
|
|
// Empty string treated as true
|
|
TEST_CHECK(cfg.set("noforget", "") == 0);
|
|
TEST_CHECK(cfg.get("noforget", &val) == 0);
|
|
TEST_CHECK(val == "true");
|
|
|
|
// remember-nodes also accepts empty string as true
|
|
fuse_cfg.remember_nodes = false;
|
|
TEST_CHECK(cfg.set("remember-nodes", "") == 0);
|
|
TEST_CHECK(cfg.get("remember-nodes", &val) == 0);
|
|
TEST_CHECK(val == "true");
|
|
|
|
// Invalid string rejected by remember-nodes
|
|
TEST_CHECK(cfg.set("remember-nodes", "notabool") == -EINVAL);
|
|
|
|
fuse_cfg.remember_nodes = old_remember_nodes;
|
|
}
|
|
|
|
|
|
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::relpath src_path;
|
|
fs::relpath dst_path;
|
|
fs::relpath 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(std::filesystem::path(tmp_dir.native()));
|
|
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(std::filesystem::path(tmp_dir.native()));
|
|
}
|
|
|
|
void
|
|
test_fs_copyfile_source_changes_cleanup_tmpfiles()
|
|
{
|
|
int rv;
|
|
int src_fd;
|
|
fs::relpath src_path;
|
|
fs::relpath dst_path;
|
|
fs::relpath tmp_dir;
|
|
std::atomic<bool> stop_mutator{false};
|
|
std::atomic<int> 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(std::filesystem::path(tmp_dir.native()));
|
|
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 += std::string(dst_path.filename());
|
|
tmp_prefix += "_";
|
|
for(const auto &entry : std::filesystem::directory_iterator(std::filesystem::path(tmp_dir.native())))
|
|
{
|
|
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(std::filesystem::path(tmp_dir.native()));
|
|
}
|
|
|
|
// Helper: build a real srcdir/dstdir pair under /tmp, with srcdir
|
|
// pre-populated to contain `rel` as a chain of nested directories.
|
|
// Returns the tmpdir; cleans up on destruction.
|
|
namespace {
|
|
struct ClonepathFixture
|
|
{
|
|
fs::relpath tmp_dir;
|
|
fs::relpath srcdir;
|
|
fs::relpath dstdir;
|
|
|
|
ClonepathFixture()
|
|
{
|
|
char tmp_template[] = "/tmp/mergerfs-test-clonepath-XXXXXX";
|
|
if(::mkdtemp(tmp_template) == nullptr)
|
|
{
|
|
TEST_CHECK(false);
|
|
return;
|
|
}
|
|
tmp_dir = tmp_template;
|
|
srcdir = tmp_dir / "src";
|
|
dstdir = tmp_dir / "dst";
|
|
::mkdir(srcdir.c_str(),0755);
|
|
::mkdir(dstdir.c_str(),0755);
|
|
}
|
|
|
|
~ClonepathFixture()
|
|
{
|
|
if(!tmp_dir.empty())
|
|
std::filesystem::remove_all(std::filesystem::path(tmp_dir.native()));
|
|
}
|
|
|
|
// Populate srcdir/<rel> as a directory chain.
|
|
bool make_src_chain(std::string_view rel)
|
|
{
|
|
fs::relpath p = srcdir;
|
|
std::size_t pos = 0;
|
|
while(pos < rel.size())
|
|
{
|
|
std::size_t next = rel.find('/',pos);
|
|
if(next == std::string_view::npos)
|
|
next = rel.size();
|
|
std::string_view component = rel.substr(pos,next - pos);
|
|
pos = next + 1;
|
|
if(component.empty())
|
|
continue;
|
|
p /= component;
|
|
if(::mkdir(p.c_str(),0755) < 0 && errno != EEXIST)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
}
|
|
|
|
void
|
|
test_fs_clonepath_empty_relpath()
|
|
{
|
|
// Empty relpath is a no-op, returns 0.
|
|
ClonepathFixture fix;
|
|
if(fix.tmp_dir.empty()) return;
|
|
|
|
int rv = fs::clonepath(fix.srcdir,fix.dstdir,fs::relpath());
|
|
TEST_CHECK(rv == 0);
|
|
}
|
|
|
|
void
|
|
test_fs_clonepath_single_component()
|
|
{
|
|
ClonepathFixture fix;
|
|
if(fix.tmp_dir.empty()) return;
|
|
|
|
TEST_CHECK(fix.make_src_chain("a"));
|
|
|
|
int rv = fs::clonepath(fix.srcdir,fix.dstdir,fs::relpath("a"));
|
|
TEST_CHECK(rv == 0);
|
|
|
|
struct stat st;
|
|
fs::relpath expect = fix.dstdir / "a";
|
|
TEST_CHECK(::lstat(expect.c_str(),&st) == 0);
|
|
TEST_CHECK(S_ISDIR(st.st_mode));
|
|
}
|
|
|
|
void
|
|
test_fs_clonepath_many_components()
|
|
{
|
|
// Stack-budget test: a 100-component relpath. The recursive version
|
|
// would have used ~12 KB * 100 = 1.2 MB of stack just for path
|
|
// objects; the iterative version uses ~8 KB total.
|
|
ClonepathFixture fix;
|
|
if(fix.tmp_dir.empty()) return;
|
|
|
|
std::string rel;
|
|
for(int i = 0; i < 100; ++i)
|
|
{
|
|
if(!rel.empty()) rel += '/';
|
|
rel += "d";
|
|
rel += std::to_string(i);
|
|
}
|
|
TEST_CHECK(fix.make_src_chain(rel));
|
|
|
|
int rv = fs::clonepath(fix.srcdir,fix.dstdir,fs::relpath(rel));
|
|
TEST_CHECK(rv == 0);
|
|
|
|
struct stat st;
|
|
fs::relpath expect = fix.dstdir / rel;
|
|
TEST_CHECK(::lstat(expect.c_str(),&st) == 0);
|
|
TEST_CHECK(S_ISDIR(st.st_mode));
|
|
}
|
|
|
|
void
|
|
test_fs_clonepath_intermediate_exists()
|
|
{
|
|
// mkdir on an already-existing intermediate dir must not fail and
|
|
// must not re-copy metadata; the walk continues to deeper components.
|
|
ClonepathFixture fix;
|
|
if(fix.tmp_dir.empty()) return;
|
|
|
|
TEST_CHECK(fix.make_src_chain("a/b/c"));
|
|
|
|
// Pre-create dstdir/a so mkdir hits EEXIST at the first step.
|
|
fs::relpath pre = fix.dstdir / "a";
|
|
TEST_CHECK(::mkdir(pre.c_str(),0700) == 0);
|
|
|
|
int rv = fs::clonepath(fix.srcdir,fix.dstdir,fs::relpath("a/b/c"));
|
|
TEST_CHECK(rv == 0);
|
|
|
|
struct stat st;
|
|
fs::relpath expect = fix.dstdir / "a/b/c";
|
|
TEST_CHECK(::lstat(expect.c_str(),&st) == 0);
|
|
TEST_CHECK(S_ISDIR(st.st_mode));
|
|
}
|
|
|
|
void
|
|
test_fs_clonepath_non_dir_intermediate()
|
|
{
|
|
// If an intermediate src component is a regular file, must return
|
|
// -ENOTDIR.
|
|
ClonepathFixture fix;
|
|
if(fix.tmp_dir.empty()) return;
|
|
|
|
TEST_CHECK(fix.make_src_chain("a"));
|
|
fs::relpath file = fix.srcdir / "a/b";
|
|
int fd = ::open(file.c_str(),O_CREAT|O_WRONLY,0644);
|
|
TEST_CHECK(fd >= 0);
|
|
if(fd >= 0) ::close(fd);
|
|
|
|
int rv = fs::clonepath(fix.srcdir,fix.dstdir,fs::relpath("a/b"));
|
|
TEST_CHECK(rv == -ENOTDIR);
|
|
}
|
|
|
|
void
|
|
test_fs_clonepath_lstat_failure()
|
|
{
|
|
// If a src component does not exist, must return -ENOENT.
|
|
ClonepathFixture fix;
|
|
if(fix.tmp_dir.empty()) return;
|
|
|
|
// Don't make any src chain; "missing/dir" doesn't exist.
|
|
int rv = fs::clonepath(fix.srcdir,fix.dstdir,fs::relpath("missing/dir"));
|
|
TEST_CHECK(rv == -ENOENT);
|
|
}
|
|
|
|
void
|
|
test_fs_clonepath_double_slash_skip()
|
|
{
|
|
// Defensive: double-slash in rel form is malformed but should not
|
|
// re-stat the same path or fail; empty segments are skipped.
|
|
ClonepathFixture fix;
|
|
if(fix.tmp_dir.empty()) return;
|
|
|
|
TEST_CHECK(fix.make_src_chain("a/b"));
|
|
|
|
// "a//b" -- the // produces an empty segment that should be skipped.
|
|
int rv = fs::clonepath(fix.srcdir,fix.dstdir,fs::relpath("a//b"));
|
|
TEST_CHECK(rv == 0);
|
|
|
|
struct stat st;
|
|
fs::relpath expect = fix.dstdir / "a/b";
|
|
TEST_CHECK(::lstat(expect.c_str(),&st) == 0);
|
|
TEST_CHECK(S_ISDIR(st.st_mode));
|
|
}
|
|
|
|
void
|
|
test_hashset_put_and_size()
|
|
{
|
|
HashSet set;
|
|
const char *names[] = {
|
|
"alpha",
|
|
"bravo",
|
|
"charlie",
|
|
"delta",
|
|
"echo",
|
|
"foxtrot",
|
|
"golf",
|
|
"hotel",
|
|
"india",
|
|
"juliet",
|
|
"kilo",
|
|
"lima",
|
|
};
|
|
const size_t names_len = (sizeof(names) / sizeof(names[0]));
|
|
const char *extra = "omega-long-filename";
|
|
|
|
TEST_CHECK(set.size() == 0);
|
|
TEST_CHECK(set.put(names[0]) == 1);
|
|
TEST_CHECK(set.put(names[0],strlen(names[0])) == 0);
|
|
|
|
for(size_t i = 1; i < names_len; ++i)
|
|
TEST_CHECK(set.put(names[i],strlen(names[i])) == 1);
|
|
|
|
TEST_CHECK(set.size() == (int)names_len);
|
|
TEST_CHECK(set.put(names[7]) == 0);
|
|
TEST_CHECK(set.put(names[11],strlen(names[11])) == 0);
|
|
TEST_CHECK(set.size() == (int)names_len);
|
|
|
|
TEST_CHECK(set.put(extra) == 1);
|
|
TEST_CHECK(set.put(extra,strlen(extra)) == 0);
|
|
TEST_CHECK(set.size() == (int)(names_len + 1));
|
|
}
|
|
|
|
void
|
|
test_str_eq_nullptr()
|
|
{
|
|
TEST_CHECK(str::eq(nullptr, nullptr) == true);
|
|
TEST_CHECK(str::eq(nullptr, "") == false);
|
|
TEST_CHECK(str::eq("", nullptr) == false);
|
|
TEST_CHECK(str::eq(nullptr, "hello") == false);
|
|
TEST_CHECK(str::eq("hello", nullptr) == false);
|
|
TEST_CHECK(str::eq(nullptr, nullptr) == true);
|
|
}
|
|
|
|
void
|
|
test_str_startswith_char_nullptr()
|
|
{
|
|
TEST_CHECK(str::startswith((const char*)nullptr, "foo") == false);
|
|
TEST_CHECK(str::startswith("foo", (const char*)nullptr) == false);
|
|
TEST_CHECK(str::startswith((const char*)nullptr, (const char*)nullptr) == false);
|
|
TEST_CHECK(str::startswith("foobar", "foo") == true);
|
|
TEST_CHECK(str::startswith("foobar", "bar") == false);
|
|
TEST_CHECK(str::startswith("", "") == true);
|
|
}
|
|
|
|
void
|
|
test_str_from_u64_suffixes()
|
|
{
|
|
u64 val;
|
|
|
|
TEST_CHECK(str::from("0", &val) == 0);
|
|
TEST_CHECK(val == 0);
|
|
|
|
TEST_CHECK(str::from("1024", &val) == 0);
|
|
TEST_CHECK(val == 1024);
|
|
|
|
TEST_CHECK(str::from("1K", &val) == 0);
|
|
TEST_CHECK(val == 1024);
|
|
TEST_CHECK(str::from("1k", &val) == 0);
|
|
TEST_CHECK(val == 1024);
|
|
|
|
TEST_CHECK(str::from("1M", &val) == 0);
|
|
TEST_CHECK(val == 1024 * 1024);
|
|
TEST_CHECK(str::from("1m", &val) == 0);
|
|
TEST_CHECK(val == 1024 * 1024);
|
|
|
|
TEST_CHECK(str::from("1G", &val) == 0);
|
|
TEST_CHECK(val == 1024ULL * 1024ULL * 1024ULL);
|
|
TEST_CHECK(str::from("1g", &val) == 0);
|
|
TEST_CHECK(val == 1024ULL * 1024ULL * 1024ULL);
|
|
|
|
TEST_CHECK(str::from("1T", &val) == 0);
|
|
TEST_CHECK(val == 1024ULL * 1024ULL * 1024ULL * 1024ULL);
|
|
TEST_CHECK(str::from("1t", &val) == 0);
|
|
TEST_CHECK(val == 1024ULL * 1024ULL * 1024ULL * 1024ULL);
|
|
|
|
TEST_CHECK(str::from("1024B", &val) == 0);
|
|
TEST_CHECK(val == 1024);
|
|
TEST_CHECK(str::from("1024b", &val) == 0);
|
|
TEST_CHECK(val == 1024);
|
|
|
|
TEST_CHECK(str::from("2K", &val) == 0);
|
|
TEST_CHECK(val == 2048);
|
|
TEST_CHECK(str::from("4M", &val) == 0);
|
|
TEST_CHECK(val == 4ULL * 1024 * 1024);
|
|
|
|
TEST_CHECK(str::from("1P", &val) == -EINVAL);
|
|
TEST_CHECK(str::from("hello", &val) == -EINVAL);
|
|
TEST_CHECK(str::from("", &val) == -EINVAL);
|
|
|
|
TEST_CHECK(str::from("16777216T", &val) == -EOVERFLOW);
|
|
|
|
TEST_CHECK(str::from("9999999999999999999999", &val) == -EINVAL);
|
|
}
|
|
|
|
void
|
|
test_str_from_s64()
|
|
{
|
|
s64 val;
|
|
|
|
TEST_CHECK(str::from("0", &val) == 0);
|
|
TEST_CHECK(val == 0);
|
|
|
|
TEST_CHECK(str::from("-1", &val) == 0);
|
|
TEST_CHECK(val == -1);
|
|
|
|
TEST_CHECK(str::from("1K", &val) == 0);
|
|
TEST_CHECK(val == 1024);
|
|
|
|
TEST_CHECK(str::from("1M", &val) == 0);
|
|
TEST_CHECK(val == 1024 * 1024);
|
|
|
|
TEST_CHECK(str::from("1G", &val) == 0);
|
|
TEST_CHECK(val == 1024LL * 1024 * 1024);
|
|
|
|
TEST_CHECK(str::from("1T", &val) == 0);
|
|
TEST_CHECK(val == 1024LL * 1024 * 1024 * 1024);
|
|
|
|
TEST_CHECK(str::from("1P", &val) == -EINVAL);
|
|
TEST_CHECK(str::from("abc", &val) == -EINVAL);
|
|
}
|
|
|
|
void
|
|
test_str_from_int()
|
|
{
|
|
int val;
|
|
|
|
TEST_CHECK(str::from("0", &val) == 0);
|
|
TEST_CHECK(val == 0);
|
|
|
|
TEST_CHECK(str::from("123", &val) == 0);
|
|
TEST_CHECK(val == 123);
|
|
|
|
TEST_CHECK(str::from("-456", &val) == 0);
|
|
TEST_CHECK(val == -456);
|
|
|
|
TEST_CHECK(str::from("1K", &val) == 0);
|
|
TEST_CHECK(val == 1);
|
|
TEST_CHECK(str::from("abc", &val) == -EINVAL);
|
|
}
|
|
|
|
void
|
|
test_num_humanize()
|
|
{
|
|
TEST_CHECK(num::humanize(0) == "0");
|
|
TEST_CHECK(num::humanize(512) == "512");
|
|
TEST_CHECK(num::humanize(1024) == "1K");
|
|
TEST_CHECK(num::humanize(2048) == "2K");
|
|
TEST_CHECK(num::humanize(1024 * 1024) == "1M");
|
|
TEST_CHECK(num::humanize(2 * 1024 * 1024) == "2M");
|
|
TEST_CHECK(num::humanize(1024ULL * 1024 * 1024) == "1G");
|
|
TEST_CHECK(num::humanize(2ULL * 1024 * 1024 * 1024) == "2G");
|
|
TEST_CHECK(num::humanize(1024ULL * 1024 * 1024 * 1024) == "1T");
|
|
TEST_CHECK(num::humanize(2ULL * 1024 * 1024 * 1024 * 1024) == "2T");
|
|
TEST_CHECK(num::humanize(1536) == "1536");
|
|
TEST_CHECK(num::humanize(1025) == "1025");
|
|
}
|
|
|
|
struct RmdirErr
|
|
{
|
|
private:
|
|
std::optional<int> _err;
|
|
|
|
public:
|
|
RmdirErr() {}
|
|
|
|
operator int()
|
|
{
|
|
return (_err.has_value() ? _err.value() : -ENOENT);
|
|
}
|
|
|
|
RmdirErr&
|
|
operator=(int v_)
|
|
{
|
|
if(!_err.has_value())
|
|
_err = ((v_ >= 0) ? 0 : v_);
|
|
else if((v_ == -EEXIST) || (v_ == -ENOTEMPTY))
|
|
_err = v_;
|
|
else if((*_err == -EEXIST) || (*_err == -ENOTEMPTY))
|
|
;
|
|
else if(v_ >= 0)
|
|
_err = 0;
|
|
else
|
|
_err = v_;
|
|
|
|
return *this;
|
|
}
|
|
|
|
bool
|
|
operator==(int v_)
|
|
{
|
|
if(_err.has_value())
|
|
return (_err.value() == v_);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
void
|
|
test_rmdir_err_default_is_enoent()
|
|
{
|
|
RmdirErr err;
|
|
TEST_CHECK((int)err == -ENOENT);
|
|
}
|
|
|
|
void
|
|
test_rmdir_err_first_success_sets_zero()
|
|
{
|
|
RmdirErr err;
|
|
err = 0;
|
|
TEST_CHECK((int)err == 0);
|
|
}
|
|
|
|
void
|
|
test_rmdir_err_first_error_sets_value()
|
|
{
|
|
RmdirErr err;
|
|
err = -EACCES;
|
|
TEST_CHECK((int)err == -EACCES);
|
|
}
|
|
|
|
void
|
|
test_rmdir_err_enotempty_overwrites_other()
|
|
{
|
|
RmdirErr err;
|
|
err = -EACCES;
|
|
err = -ENOTEMPTY;
|
|
TEST_CHECK((int)err == -ENOTEMPTY);
|
|
}
|
|
|
|
void
|
|
test_rmdir_err_eexist_overwrites_other()
|
|
{
|
|
RmdirErr err;
|
|
err = -EACCES;
|
|
err = -EEXIST;
|
|
TEST_CHECK((int)err == -EEXIST);
|
|
}
|
|
|
|
void
|
|
test_rmdir_err_enotempty_not_overwritten_by_other()
|
|
{
|
|
RmdirErr err;
|
|
err = -ENOTEMPTY;
|
|
err = -EACCES;
|
|
TEST_CHECK((int)err == -ENOTEMPTY);
|
|
}
|
|
|
|
void
|
|
test_rmdir_err_eexist_not_overwritten_by_other()
|
|
{
|
|
RmdirErr err;
|
|
err = -EEXIST;
|
|
err = -EACCES;
|
|
TEST_CHECK((int)err == -EEXIST);
|
|
}
|
|
|
|
void
|
|
test_rmdir_err_success_does_not_overwrite_priority()
|
|
{
|
|
RmdirErr err;
|
|
err = -ENOTEMPTY;
|
|
err = 0;
|
|
TEST_CHECK((int)err == -ENOTEMPTY);
|
|
|
|
RmdirErr err2;
|
|
err2 = -EEXIST;
|
|
err2 = 0;
|
|
TEST_CHECK((int)err2 == -EEXIST);
|
|
}
|
|
|
|
void
|
|
test_rmdir_err_success_overwrites_generic_error()
|
|
{
|
|
RmdirErr err;
|
|
err = -EACCES;
|
|
err = 0;
|
|
TEST_CHECK((int)err == 0);
|
|
}
|
|
|
|
void
|
|
test_rmdir_err_enotempty_overwrites_enotempty()
|
|
{
|
|
RmdirErr err;
|
|
err = -ENOTEMPTY;
|
|
err = -EEXIST;
|
|
TEST_CHECK((int)err == -EEXIST);
|
|
}
|
|
|
|
void
|
|
test_hashset_inline_to_heap_growth()
|
|
{
|
|
HashSet set;
|
|
|
|
for(int i = 0; i < 20; ++i)
|
|
{
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "key_%d", i);
|
|
TEST_CHECK(set.put(buf) == 1);
|
|
}
|
|
|
|
TEST_CHECK(set.size() == 20);
|
|
|
|
for(int i = 0; i < 20; ++i)
|
|
{
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "key_%d", i);
|
|
TEST_CHECK(set.put(buf) == 0);
|
|
}
|
|
|
|
TEST_CHECK(set.size() == 20);
|
|
}
|
|
|
|
void
|
|
test_hashset_empty_string()
|
|
{
|
|
HashSet set;
|
|
|
|
TEST_CHECK(set.put("") == 1);
|
|
TEST_CHECK(set.put("") == 0);
|
|
TEST_CHECK(set.size() == 1);
|
|
}
|
|
|
|
void
|
|
test_hashset_many_items()
|
|
{
|
|
HashSet set;
|
|
|
|
for(int i = 0; i < 100; ++i)
|
|
{
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "longer_key_name_%d", i);
|
|
TEST_CHECK(set.put(buf) == 1);
|
|
}
|
|
|
|
TEST_CHECK(set.size() == 100);
|
|
|
|
for(int i = 0; i < 100; ++i)
|
|
{
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "longer_key_name_%d", i);
|
|
TEST_CHECK(set.put(buf) == 0);
|
|
}
|
|
|
|
TEST_CHECK(set.size() == 100);
|
|
}
|
|
|
|
void
|
|
test_fs_inode_set_algo_valid()
|
|
{
|
|
const std::vector<std::string> algos = {
|
|
"passthrough",
|
|
"path-hash",
|
|
"path-hash32",
|
|
"devino-hash",
|
|
"devino-hash32",
|
|
"hybrid-hash",
|
|
"hybrid-hash32",
|
|
};
|
|
|
|
for(const auto &algo : algos)
|
|
{
|
|
TEST_CHECK(fs::inode::set_algo(algo) == 0);
|
|
TEST_CHECK(fs::inode::get_algo() == algo);
|
|
}
|
|
}
|
|
|
|
void
|
|
test_fs_inode_set_algo_invalid()
|
|
{
|
|
TEST_CHECK(fs::inode::set_algo("nope") == -EINVAL);
|
|
TEST_CHECK(fs::inode::set_algo("") == -EINVAL);
|
|
TEST_CHECK(fs::inode::set_algo("HYBRID-HASH") == -EINVAL);
|
|
}
|
|
|
|
void
|
|
test_fs_inode_passthrough_returns_raw_ino()
|
|
{
|
|
TEST_CHECK(fs::inode::set_algo("passthrough") == 0);
|
|
|
|
const std::string branch("/mnt/disk1");
|
|
const fs::relpath fusepath("some/file");
|
|
|
|
TEST_CHECK(fs::inode::calc(branch, fusepath, S_IFREG, 42) == 42);
|
|
TEST_CHECK(fs::inode::calc(branch, fusepath, S_IFDIR, 999) == 999);
|
|
TEST_CHECK(fs::inode::calc(branch, fusepath, S_IFREG, 1) == 1);
|
|
}
|
|
|
|
void
|
|
test_fs_inode_different_algos_produce_distinct_inodes()
|
|
{
|
|
const std::string branch("/mnt/disk1");
|
|
const fs::relpath fusepath("some/file");
|
|
const std::vector<std::string> algos = {
|
|
"path-hash",
|
|
"devino-hash",
|
|
"hybrid-hash",
|
|
};
|
|
|
|
std::vector<u64> inodes;
|
|
for(const auto &algo : algos)
|
|
{
|
|
TEST_CHECK(fs::inode::set_algo(algo) == 0);
|
|
inodes.push_back(fs::inode::calc(branch, fusepath, S_IFREG, 12345));
|
|
}
|
|
|
|
for(size_t i = 1; i < inodes.size(); ++i)
|
|
TEST_CHECK(inodes[i] != inodes[0]);
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_default_construct()
|
|
{
|
|
fs::relpath p;
|
|
TEST_CHECK(p.size() == 0);
|
|
TEST_CHECK(p.empty());
|
|
TEST_CHECK(p.c_str()[0] == '\0');
|
|
TEST_CHECK(p.capacity() == fs::relpath::BUF_SIZE - fs::relpath::REL_OFFSET - 1);
|
|
TEST_CHECK(p.native() == std::string_view{});
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_construct_from_strings()
|
|
{
|
|
fs::relpath a("/mnt/disk1");
|
|
TEST_CHECK(a.size() == 10);
|
|
TEST_CHECK(a.native() == "/mnt/disk1");
|
|
TEST_CHECK(std::strcmp(a.c_str(),"/mnt/disk1") == 0);
|
|
|
|
std::string s = "/foo/bar";
|
|
fs::relpath b(s);
|
|
TEST_CHECK(b == "/foo/bar");
|
|
|
|
std::string_view sv = "baz";
|
|
fs::relpath c(sv);
|
|
TEST_CHECK(c == "baz");
|
|
|
|
fs::relpath empty1((const char*)nullptr);
|
|
TEST_CHECK(empty1.empty());
|
|
|
|
fs::relpath empty2("");
|
|
TEST_CHECK(empty2.empty());
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_concat_canonical()
|
|
{
|
|
// Canonical contract: base has no trailing '/', rel has no leading '/'.
|
|
// concat inserts exactly one '/' between non-empty base and rel.
|
|
TEST_CHECK(fs::relpath::concat("/mnt/disk1","foo/bar") == "/mnt/disk1/foo/bar");
|
|
TEST_CHECK(fs::relpath::concat("a","b") == "a/b");
|
|
|
|
// Empty handling: empty side passes the other through, no separator.
|
|
TEST_CHECK(fs::relpath::concat("","foo") == "foo");
|
|
TEST_CHECK(fs::relpath::concat("/mnt/disk1","") == "/mnt/disk1");
|
|
TEST_CHECK(fs::relpath::concat("","").empty());
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_assign_concat_reuses_capacity()
|
|
{
|
|
fs::relpath p;
|
|
const std::size_t cap = p.capacity();
|
|
|
|
p.assign_concat("/mnt/disk1","foo/bar");
|
|
TEST_CHECK(p == "/mnt/disk1/foo/bar");
|
|
TEST_CHECK(p.capacity() == cap); // no growth
|
|
|
|
p.assign_concat("/a","b");
|
|
TEST_CHECK(p == "/a/b");
|
|
TEST_CHECK(p.capacity() == cap);
|
|
|
|
p.assign_concat("/mnt/disk1","much/longer/sub/path");
|
|
TEST_CHECK(p == "/mnt/disk1/much/longer/sub/path");
|
|
TEST_CHECK(p.capacity() == cap);
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_operator_slash()
|
|
{
|
|
fs::relpath branch("/mnt/disk1");
|
|
fs::relpath full = branch / "foo/bar";
|
|
TEST_CHECK(full == "/mnt/disk1/foo/bar");
|
|
|
|
fs::relpath p2("/a");
|
|
p2 /= "b";
|
|
TEST_CHECK(p2 == "/a/b");
|
|
|
|
// Free-function form via two string_views.
|
|
fs::relpath mixed = fs::operator/(std::string_view("/x"),std::string_view("y"));
|
|
TEST_CHECK(mixed == "/x/y");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_filename()
|
|
{
|
|
TEST_CHECK(fs::relpath("/foo/bar").filename() == "bar");
|
|
TEST_CHECK(fs::relpath("/foo/").filename() == std::string_view{});
|
|
TEST_CHECK(fs::relpath("/").filename() == std::string_view{});
|
|
TEST_CHECK(fs::relpath("foo").filename() == "foo");
|
|
TEST_CHECK(fs::relpath("").filename() == std::string_view{});
|
|
TEST_CHECK(fs::relpath("foo.txt").filename() == "foo.txt");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_parent_path()
|
|
{
|
|
TEST_CHECK(fs::relpath("/foo/bar").parent_path() == "/foo");
|
|
TEST_CHECK(fs::relpath("/foo").parent_path() == "/");
|
|
TEST_CHECK(fs::relpath("/").parent_path() == std::string_view{});
|
|
TEST_CHECK(fs::relpath("foo").parent_path() == std::string_view{});
|
|
TEST_CHECK(fs::relpath("").parent_path() == std::string_view{});
|
|
TEST_CHECK(fs::relpath("/foo/bar/").parent_path() == "/foo/bar");
|
|
TEST_CHECK(fs::relpath("a/b/c").parent_path() == "a/b");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_extension_and_stem()
|
|
{
|
|
fs::relpath a("/foo/bar.txt");
|
|
TEST_CHECK(a.extension() == ".txt");
|
|
TEST_CHECK(a.stem() == "bar");
|
|
|
|
fs::relpath b("/foo/.hidden");
|
|
TEST_CHECK(b.extension() == std::string_view{});
|
|
TEST_CHECK(b.stem() == ".hidden");
|
|
|
|
fs::relpath c("/foo/archive.tar.gz");
|
|
TEST_CHECK(c.extension() == ".gz");
|
|
TEST_CHECK(c.stem() == "archive.tar");
|
|
|
|
fs::relpath d("/foo/noext");
|
|
TEST_CHECK(d.extension() == std::string_view{});
|
|
TEST_CHECK(d.stem() == "noext");
|
|
|
|
fs::relpath dot("/foo/.");
|
|
TEST_CHECK(dot.extension() == std::string_view{});
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_remove_filename()
|
|
{
|
|
fs::relpath p("/foo/bar");
|
|
p.remove_filename();
|
|
TEST_CHECK(p == "/foo/");
|
|
|
|
fs::relpath q("/foo/");
|
|
q.remove_filename();
|
|
TEST_CHECK(q == "/foo/");
|
|
|
|
fs::relpath r("foo");
|
|
r.remove_filename();
|
|
TEST_CHECK(r == "");
|
|
|
|
fs::relpath s("/");
|
|
s.remove_filename();
|
|
TEST_CHECK(s == "/");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_lexically_relative()
|
|
{
|
|
// Same root: simple suffix.
|
|
fs::relpath a("/mnt/disk1/data/file.txt");
|
|
TEST_CHECK(a.lexically_relative("/mnt/disk1") == "data/file.txt");
|
|
|
|
// Real symlink-relativization use case from fuse_link.cpp / fuse_rename.cpp:
|
|
// target relative to the directory holding the link.
|
|
fs::relpath target("/mnt/disk1/data/file.txt");
|
|
TEST_CHECK(target.lexically_relative("/mnt/disk1/links")
|
|
== "../data/file.txt");
|
|
|
|
// Identical paths give ".".
|
|
TEST_CHECK(fs::relpath("/a/b").lexically_relative("/a/b") == ".");
|
|
|
|
// Multiple ".." steps.
|
|
TEST_CHECK(fs::relpath("/x").lexically_relative("/a/b/c") == "../../../x");
|
|
|
|
// ".." in base disqualifies the result -- empty so the caller can
|
|
// distinguish from "" / "." (a successful relativization never
|
|
// produces empty; it returns "." for equal paths).
|
|
fs::relpath dotdot_in_base = fs::relpath("/a").lexically_relative("/a/..");
|
|
TEST_CHECK(dotdot_in_base.empty());
|
|
|
|
// Mismatched absoluteness also yields empty.
|
|
fs::relpath shape_mismatch = fs::relpath("/a").lexically_relative("a");
|
|
TEST_CHECK(shape_mismatch.empty());
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_copy_and_move()
|
|
{
|
|
fs::relpath a("/mnt/disk1/foo");
|
|
fs::relpath b = a;
|
|
TEST_CHECK(b == a);
|
|
TEST_CHECK(b == "/mnt/disk1/foo");
|
|
|
|
fs::relpath c;
|
|
c = a;
|
|
TEST_CHECK(c == a);
|
|
|
|
fs::relpath d(std::move(b));
|
|
TEST_CHECK(d == "/mnt/disk1/foo");
|
|
|
|
fs::relpath e;
|
|
e = std::move(d);
|
|
TEST_CHECK(e == "/mnt/disk1/foo");
|
|
|
|
// Self-assignment is a no-op.
|
|
e = e;
|
|
TEST_CHECK(e == "/mnt/disk1/foo");
|
|
e = std::move(e);
|
|
TEST_CHECK(e == "/mnt/disk1/foo");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_equality()
|
|
{
|
|
fs::relpath a("/a/b");
|
|
fs::relpath b("/a/b");
|
|
fs::relpath c("/a/c");
|
|
|
|
TEST_CHECK(a == b);
|
|
TEST_CHECK(!(a == c));
|
|
TEST_CHECK(a != c);
|
|
TEST_CHECK(a == std::string_view("/a/b"));
|
|
TEST_CHECK(a != std::string_view("/x"));
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_implicit_conversions()
|
|
{
|
|
// The two implicit conversions are the load-bearing pieces of the
|
|
// drop-in story. Confirm they pick up correctly in overload resolution.
|
|
fs::relpath p("/etc/hostname");
|
|
|
|
// const char* selection: pass to a function taking const char*.
|
|
auto strlen_via_cstr = [](const char *s) { return std::strlen(s); };
|
|
TEST_CHECK(strlen_via_cstr(p) == p.size());
|
|
|
|
// string_view selection.
|
|
auto svsize = [](std::string_view sv) { return sv.size(); };
|
|
TEST_CHECK(svsize(p) == p.size());
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_resize_and_append()
|
|
{
|
|
// ReaddirCalc-style: pin a prefix, vary the suffix.
|
|
fs::relpath p("/mnt/disk1/");
|
|
const std::size_t prefix = p.size();
|
|
|
|
p.append("foo",3);
|
|
TEST_CHECK(p == "/mnt/disk1/foo");
|
|
|
|
p.resize(prefix);
|
|
TEST_CHECK(p == "/mnt/disk1/");
|
|
|
|
p.append(std::string_view("longer_name"));
|
|
TEST_CHECK(p == "/mnt/disk1/longer_name");
|
|
|
|
p.clear();
|
|
TEST_CHECK(p.empty());
|
|
TEST_CHECK(p.c_str()[0] == '\0');
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_construct_stores_verbatim()
|
|
{
|
|
// _init stores input verbatim; no leading-slash strip. This lets
|
|
// fs::relpath also hold a fully-built absolute fullpath without
|
|
// losing the leading '/'.
|
|
fs::relpath abs("/mnt/disk1");
|
|
TEST_CHECK(abs == "/mnt/disk1");
|
|
TEST_CHECK(abs.size() == 10);
|
|
|
|
fs::relpath rel("foo/bar");
|
|
TEST_CHECK(rel == "foo/bar");
|
|
|
|
// Empty starts empty.
|
|
fs::relpath empty;
|
|
TEST_CHECK(empty.empty());
|
|
|
|
// assign() replaces verbatim too.
|
|
fs::relpath p("foo");
|
|
p.assign("/etc");
|
|
TEST_CHECK(p == "/etc");
|
|
p.assign("bar");
|
|
TEST_CHECK(p == "bar");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_set_prefix()
|
|
{
|
|
// Hot-path use case: a fusepath stored as canonical rel (no leading
|
|
// slash), branch_path written into its prefix area in place.
|
|
// Canonical form throughout (no trailing slash on prefix, no leading
|
|
// slash on rel).
|
|
fs::relpath p("foo/bar");
|
|
TEST_CHECK(p == "foo/bar");
|
|
|
|
p.set_prefix("/mnt/disk1");
|
|
TEST_CHECK(p == "/mnt/disk1/foo/bar");
|
|
|
|
// Replace prefix in place.
|
|
p.set_prefix("/mnt/disk2");
|
|
TEST_CHECK(p == "/mnt/disk2/foo/bar");
|
|
|
|
// Clear prefix returns to the rel.
|
|
p.clear_prefix();
|
|
TEST_CHECK(p == "foo/bar");
|
|
|
|
// Empty prefix is equivalent to clear_prefix.
|
|
p.set_prefix("/etc");
|
|
TEST_CHECK(p == "/etc/foo/bar");
|
|
p.set_prefix("");
|
|
TEST_CHECK(p == "foo/bar");
|
|
|
|
// Empty rel: prefix becomes the whole path with no separator.
|
|
fs::relpath e;
|
|
e.set_prefix("/mnt/disk1");
|
|
TEST_CHECK(e == "/mnt/disk1");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_concat()
|
|
{
|
|
// concat / assign_concat splice base + '/' + rel, no stripping.
|
|
// The result is itself an fs::relpath that can be re-prefixed.
|
|
fs::relpath fullpath = fs::relpath::concat("/mnt/disk1","foo/bar");
|
|
TEST_CHECK(fullpath == "/mnt/disk1/foo/bar");
|
|
|
|
// Re-prefix the result. set_prefix replaces the full prefix area
|
|
// (which currently contains "/mnt/disk1/").
|
|
fullpath.set_prefix("/mnt/disk2");
|
|
TEST_CHECK(fullpath == "/mnt/disk2/foo/bar");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_prepend()
|
|
{
|
|
// libfuse-style backward build: walk leaf->root, prepending each
|
|
// ancestor name as "/name". prepend() writes the slash automatically.
|
|
fs::relpath p;
|
|
TEST_CHECK(p.empty());
|
|
|
|
p.prepend("leaf.txt");
|
|
TEST_CHECK(p == "/leaf.txt");
|
|
TEST_CHECK(p.size() == 9);
|
|
|
|
p.prepend("intermediate");
|
|
TEST_CHECK(p == "/intermediate/leaf.txt");
|
|
|
|
p.prepend("root");
|
|
TEST_CHECK(p == "/root/intermediate/leaf.txt");
|
|
|
|
// Single-character names work too.
|
|
fs::relpath q;
|
|
q.prepend("c");
|
|
q.prepend("b");
|
|
q.prepend("a");
|
|
TEST_CHECK(q == "/a/b/c");
|
|
|
|
// Empty-string prepend just adds a slash.
|
|
fs::relpath r;
|
|
r.prepend("");
|
|
TEST_CHECK(r == "/");
|
|
TEST_CHECK(r.size() == 1);
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_consolidate_to_rel()
|
|
{
|
|
// After backward-build via prepend(), data lives at some _start
|
|
// before REL_OFFSET. consolidate_to_rel() shifts it to the REL anchor
|
|
// so set_prefix() can be applied by callers downstream.
|
|
fs::relpath p;
|
|
p.prepend("leaf.txt");
|
|
p.prepend("dir");
|
|
TEST_CHECK(p == "/dir/leaf.txt");
|
|
|
|
// Default: keep the leading '/'.
|
|
p.consolidate_to_rel();
|
|
TEST_CHECK(p == "/dir/leaf.txt");
|
|
|
|
// strip_leading_slash=true: drop the '/' that prepend left at the
|
|
// front.
|
|
fs::relpath q;
|
|
q.prepend("file");
|
|
q.prepend("dir");
|
|
TEST_CHECK(q == "/dir/file");
|
|
|
|
q.consolidate_to_rel(/*strip_leading_slash=*/true);
|
|
TEST_CHECK(q == "dir/file");
|
|
|
|
// Now in canonical mergerfs form: rel without leading '/'. set_prefix
|
|
// is applied in place by the caller's per-branch loop.
|
|
q.set_prefix("/branch");
|
|
TEST_CHECK(q == "/branch/dir/file");
|
|
|
|
// Idempotent: consolidating an already-canonical-rel path is a no-op.
|
|
fs::relpath r("foo/bar");
|
|
r.consolidate_to_rel();
|
|
TEST_CHECK(r == "foo/bar");
|
|
|
|
// Empty path: strip_leading_slash should be safe (no leading '/' to
|
|
// strip, no underflow).
|
|
fs::relpath s;
|
|
s.consolidate_to_rel(/*strip_leading_slash=*/true);
|
|
TEST_CHECK(s.empty());
|
|
|
|
const std::size_t rel_capacity = fs::relpath::BUF_SIZE - fs::relpath::REL_OFFSET - 1;
|
|
|
|
// Boundary: enough components to land exactly at REL_OFFSET bytes
|
|
// (each prepend adds component_size + 1 for the slash). After
|
|
// strip_leading_slash this is exactly rel_capacity. Math is
|
|
// parameterized off REL_OFFSET so this test scales with the buffer
|
|
// size; with the symmetric 8 KB layout this is 4 components of 1023
|
|
// bytes each = 4096 bytes total, consolidating to 4095 = rel_capacity.
|
|
constexpr std::size_t component_count = 4;
|
|
static_assert((fs::relpath::REL_OFFSET % component_count) == 0,
|
|
"boundary test assumes REL_OFFSET divides evenly");
|
|
const std::size_t component_size = (fs::relpath::REL_OFFSET / component_count) - 1;
|
|
const std::string max_component(component_size,'m');
|
|
fs::relpath max_rel;
|
|
for(std::size_t i = 0; i < component_count; ++i)
|
|
max_rel.prepend(max_component);
|
|
TEST_CHECK(max_rel.size() == rel_capacity + 1);
|
|
|
|
max_rel.consolidate_to_rel(/*strip_leading_slash=*/true);
|
|
TEST_CHECK(max_rel.size() == rel_capacity);
|
|
TEST_CHECK(max_rel.prefix().empty());
|
|
TEST_CHECK(max_rel.rel().size() == rel_capacity);
|
|
|
|
// Note: with the symmetric BUF_SIZE = 2 * REL_OFFSET layout, the
|
|
// "prepend fits in the prefix area but consolidate_to_rel overflows
|
|
// the rel area" window collapses to a single byte and is unreachable
|
|
// with strip_leading_slash=true (the strip absorbs the off-by-one).
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_clear_resets_state()
|
|
{
|
|
// clear() must return to the canonical empty state regardless of
|
|
// prior contents, so that subsequent prepend() works. This is the
|
|
// libfuse try_get_path contract.
|
|
fs::relpath p("/mnt/disk1/foo");
|
|
TEST_CHECK(!p.empty());
|
|
|
|
p.clear();
|
|
TEST_CHECK(p.empty());
|
|
|
|
// prepend() must work after clear().
|
|
p.prepend("file");
|
|
p.prepend("dir");
|
|
TEST_CHECK(p == "/dir/file");
|
|
|
|
// Round-trip through consolidate_to_rel(strip=true) → set_prefix.
|
|
p.consolidate_to_rel(/*strip_leading_slash=*/true);
|
|
TEST_CHECK(p == "dir/file");
|
|
p.set_prefix("/branch");
|
|
TEST_CHECK(p == "/branch/dir/file");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_self_aliasing()
|
|
{
|
|
// assign_concat / operator/= must handle the case where one of the
|
|
// inputs aliases *this's own buffer (e.g. `p /= "x"`). Naive ordering
|
|
// writes the rel slot first, which corrupts the base if base aliases.
|
|
fs::relpath a("a");
|
|
a /= "b";
|
|
TEST_CHECK(a == "a/b");
|
|
|
|
fs::relpath foo("foo/bar");
|
|
foo /= "baz";
|
|
TEST_CHECK(foo == "foo/bar/baz");
|
|
|
|
// Leading-slash self-aliasing too.
|
|
fs::relpath abs("/mnt/disk1");
|
|
abs /= "data";
|
|
TEST_CHECK(abs == "/mnt/disk1/data");
|
|
|
|
// Repeated /=
|
|
fs::relpath p("a");
|
|
p /= "b";
|
|
p /= "c";
|
|
p /= "d";
|
|
TEST_CHECK(p == "a/b/c/d");
|
|
|
|
// assign_concat with native() as the base
|
|
fs::relpath q("rel");
|
|
q.assign_concat(q.native(),"more");
|
|
TEST_CHECK(q == "rel/more");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_prefix_and_rel_accessors()
|
|
{
|
|
// Path with no prefix set: prefix() empty, rel() is the whole thing.
|
|
fs::relpath p("foo/bar");
|
|
TEST_CHECK(p.prefix().empty());
|
|
TEST_CHECK(p.rel() == "foo/bar");
|
|
|
|
// After set_prefix the views reflect each region.
|
|
p.set_prefix("/mnt/disk1");
|
|
TEST_CHECK(p.prefix() == "/mnt/disk1/");
|
|
TEST_CHECK(p.rel() == "foo/bar");
|
|
TEST_CHECK(p == "/mnt/disk1/foo/bar"); // native is the concatenation
|
|
|
|
// clear_prefix resets prefix() to empty.
|
|
p.clear_prefix();
|
|
TEST_CHECK(p.prefix().empty());
|
|
TEST_CHECK(p.rel() == "foo/bar");
|
|
|
|
// For a path constructed verbatim with a leading slash, the data
|
|
// sits at REL_OFFSET as the rel; prefix() is empty.
|
|
fs::relpath abs("/etc/hostname");
|
|
TEST_CHECK(abs.prefix().empty());
|
|
TEST_CHECK(abs.rel() == "/etc/hostname");
|
|
TEST_CHECK(abs.rel() == abs.native());
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_resize_zero_fills()
|
|
{
|
|
// Growing via resize must zero-fill the new bytes so a subsequent
|
|
// read of [old_size, new_size) doesn't return garbage from a
|
|
// previous use of the buffer.
|
|
fs::relpath p("foo");
|
|
p.append("dirty",5); // _buf now contains "foodirty"
|
|
TEST_CHECK(p == "foodirty");
|
|
|
|
p.resize(3); // back to "foo"
|
|
TEST_CHECK(p == "foo");
|
|
TEST_CHECK(p.size() == 3);
|
|
|
|
// Grow back to 8 bytes. Bytes 3..7 must be '\0', not the leftover
|
|
// "dirty" content.
|
|
p.resize(8);
|
|
TEST_CHECK(p.size() == 8);
|
|
// The whole path read as a string_view should be "foo\0\0\0\0\0".
|
|
TEST_CHECK(p.native().size() == 8);
|
|
for(std::size_t i = 3; i < 8; i++)
|
|
TEST_CHECK(p.native()[i] == '\0');
|
|
// c_str() stops at the first '\0' so it reads "foo".
|
|
TEST_CHECK(std::strcmp(p.c_str(),"foo") == 0);
|
|
|
|
// Shrinking truncates and writes a NUL at the new end.
|
|
p.resize(2);
|
|
TEST_CHECK(p == "fo");
|
|
TEST_CHECK(p.c_str()[2] == '\0');
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_rel_size_invariant()
|
|
{
|
|
// _rel_size is internal but visible through rel(). Verify it stays
|
|
// consistent across the full set of mutations: assign_concat,
|
|
// set_prefix (must NOT touch rel size), clear_prefix, append,
|
|
// resize, remove_filename, clear, copy/move, swap.
|
|
fs::relpath p;
|
|
p.assign_concat("/mnt/disk1","foo/bar");
|
|
TEST_CHECK(p.rel() == "foo/bar"); // rel cached as 7
|
|
TEST_CHECK(p.rel().size() == 7);
|
|
|
|
// set_prefix only changes the prefix area; rel must be unchanged.
|
|
p.set_prefix("/mnt/disk2");
|
|
TEST_CHECK(p.rel() == "foo/bar");
|
|
TEST_CHECK(p.rel().size() == 7);
|
|
|
|
// Replace with a different-length prefix; rel still unchanged.
|
|
p.set_prefix("/x");
|
|
TEST_CHECK(p == "/x/foo/bar");
|
|
TEST_CHECK(p.rel() == "foo/bar");
|
|
|
|
// clear_prefix drops prefix; rel unchanged.
|
|
p.clear_prefix();
|
|
TEST_CHECK(p == "foo/bar");
|
|
TEST_CHECK(p.rel() == "foo/bar");
|
|
TEST_CHECK(p.size() == 7);
|
|
|
|
// append extends the rel.
|
|
p.append("/baz",4);
|
|
TEST_CHECK(p == "foo/bar/baz");
|
|
TEST_CHECK(p.rel() == "foo/bar/baz");
|
|
|
|
// resize shrinks; rel shrinks with it.
|
|
p.resize(3);
|
|
TEST_CHECK(p == "foo");
|
|
TEST_CHECK(p.rel() == "foo");
|
|
|
|
// remove_filename truncates to the directory form.
|
|
p.assign_concat("/mnt","a/b");
|
|
TEST_CHECK(p == "/mnt/a/b");
|
|
TEST_CHECK(p.rel() == "a/b");
|
|
p.remove_filename();
|
|
TEST_CHECK(p == "/mnt/a/");
|
|
TEST_CHECK(p.rel() == "a/");
|
|
|
|
// Construct from a leading-'/' literal: stored verbatim, so the
|
|
// entire payload is the rel.
|
|
fs::relpath abs("/etc/hostname");
|
|
TEST_CHECK(abs.rel() == "/etc/hostname");
|
|
|
|
// clear resets size; rel is empty.
|
|
abs.clear();
|
|
TEST_CHECK(abs.empty());
|
|
TEST_CHECK(abs.rel().empty());
|
|
|
|
// Copy/move preserve _rel_size (visible via rel()).
|
|
fs::relpath src;
|
|
src.assign_concat("/p","r");
|
|
fs::relpath cpy = src;
|
|
TEST_CHECK(cpy.rel() == "r");
|
|
fs::relpath mv = std::move(src);
|
|
TEST_CHECK(mv.rel() == "r");
|
|
|
|
// Swap exchanges _rel_size too.
|
|
fs::relpath a;
|
|
a.assign_concat("/x","aaa");
|
|
fs::relpath b;
|
|
b.assign_concat("/y","bbbb");
|
|
a.swap(b);
|
|
TEST_CHECK(a.rel() == "bbbb");
|
|
TEST_CHECK(b.rel() == "aaa");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_swap()
|
|
{
|
|
// Swap two paths with content in both the prefix area and rel.
|
|
fs::relpath a = fs::relpath::concat("/x","mnt/disk1");
|
|
fs::relpath b("foo/bar");
|
|
TEST_CHECK(a == "/x/mnt/disk1");
|
|
TEST_CHECK(b == "foo/bar");
|
|
|
|
a.swap(b);
|
|
TEST_CHECK(a == "foo/bar");
|
|
TEST_CHECK(b == "/x/mnt/disk1");
|
|
|
|
// Swap between leading-'/' and rel-form paths.
|
|
fs::relpath abs("/etc/hostname");
|
|
fs::relpath rel("relpath");
|
|
abs.swap(rel);
|
|
TEST_CHECK(abs == "relpath");
|
|
TEST_CHECK(rel == "/etc/hostname");
|
|
|
|
// Self-swap is a no-op.
|
|
fs::relpath c("self");
|
|
c.swap(c);
|
|
TEST_CHECK(c == "self");
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_set_prefix_oversize()
|
|
{
|
|
// Oversized-prefix fallback: when a prefix won't fit in front of
|
|
// the default rel anchor (REL_OFFSET), set_prefix shifts the rel
|
|
// rightward so the full BUF_SIZE-1 can be used. Unreachable under
|
|
// Linux PATH_MAX but the class supports it for correctness.
|
|
fs::relpath p("foo");
|
|
TEST_CHECK(p.size() == 3);
|
|
TEST_CHECK(p.rel() == "foo");
|
|
|
|
// 5000-byte prefix exceeds REL_OFFSET (4096) but the total path
|
|
// (5000 + 1 + 3) fits in BUF_SIZE-1.
|
|
const std::size_t big_prefix_len = 5000;
|
|
std::string big_prefix(big_prefix_len,'A');
|
|
p.set_prefix(big_prefix);
|
|
TEST_CHECK(p.size() == big_prefix_len + 1 + 3);
|
|
TEST_CHECK(p.native().substr(0,big_prefix_len) == big_prefix);
|
|
TEST_CHECK(p.native().substr(big_prefix_len) == "/foo");
|
|
TEST_CHECK(p.rel() == "foo");
|
|
TEST_CHECK(p.prefix() == big_prefix + "/");
|
|
|
|
// A subsequent fitting prefix still works (rel stays at its grown
|
|
// anchor; this is allowed since the prefix area can be larger than
|
|
// REL_OFFSET without harm). Verify the prefix slice and that several
|
|
// back-to-back fast-path calls in a row leave a clean buffer (the
|
|
// per-branch loop pattern).
|
|
p.set_prefix("/branch");
|
|
TEST_CHECK(p == "/branch/foo");
|
|
TEST_CHECK(p.rel() == "foo");
|
|
TEST_CHECK(p.prefix() == "/branch/");
|
|
|
|
p.set_prefix("/another");
|
|
TEST_CHECK(p == "/another/foo");
|
|
TEST_CHECK(p.rel() == "foo");
|
|
TEST_CHECK(p.prefix() == "/another/");
|
|
|
|
p.set_prefix("/x");
|
|
TEST_CHECK(p == "/x/foo");
|
|
TEST_CHECK(p.rel() == "foo");
|
|
TEST_CHECK(p.prefix() == "/x/");
|
|
|
|
// Oversized prefix with empty rel: no separator, prefix becomes the
|
|
// whole path, can fill up to BUF_SIZE-1.
|
|
fs::relpath e;
|
|
std::string max_prefix(fs::relpath::BUF_SIZE - 1,'C');
|
|
e.set_prefix(max_prefix);
|
|
TEST_CHECK(e.size() == fs::relpath::BUF_SIZE - 1);
|
|
TEST_CHECK(e.native() == max_prefix);
|
|
}
|
|
|
|
void
|
|
test_fs_relpath_root_nodeid_dispatch_contract()
|
|
{
|
|
// libfuse's try_get_path() walks leaf-to-root. Two cases produce an
|
|
// empty fs::relpath after the call:
|
|
// 1. nodeid == FUSE_ROOT_ID: the walk has zero components to
|
|
// prepend, so an empty path is the legitimate result. The
|
|
// dispatcher (fuse_lib_getattr / fuse_lib_setattr /
|
|
// fuse_lib_statx_path) issues a path-based op for this case.
|
|
// 2. ESTALE was masked above (a nameless node, e.g. an unlinked
|
|
// open file): try_get_path's error path resets the buffer to
|
|
// empty, so the caller observes empty here too. The
|
|
// dispatcher falls back to the fh-based op (fXXX(0)) which
|
|
// lets mergerfs find the file via OpenFiles by handle.
|
|
//
|
|
// The dispatcher discriminator is (nodeid == FUSE_ROOT_ID), not
|
|
// empty() alone. Lock the no-allocation, no-leading-slash contract
|
|
// of the empty path here so a future change to clear() / prepend() /
|
|
// consolidate_to_rel() doesn't silently flip the discriminator.
|
|
fs::relpath p;
|
|
p.clear(); // libfuse start state
|
|
p.consolidate_to_rel(/*strip_leading_slash=*/false); // zero prepends
|
|
TEST_CHECK(p.empty());
|
|
TEST_CHECK(p.size() == 0);
|
|
TEST_CHECK(p.native() == std::string_view{});
|
|
TEST_CHECK(p.c_str()[0] == '\0');
|
|
|
|
// The path-vs-fh dispatcher uses empty() as part of the test; make
|
|
// sure the implicit conversion to const char* yields a non-null,
|
|
// empty C string (callers may pass it to fs::* wrappers that take
|
|
// const char*).
|
|
const char *cs = p;
|
|
TEST_CHECK(cs != nullptr);
|
|
TEST_CHECK(cs[0] == '\0');
|
|
|
|
// After consolidate_to_rel(strip=true) on a zero-prepend path the
|
|
// result is also empty (the strip is a no-op when the first byte
|
|
// isn't '/'; _size > 0 guards prevent underflow).
|
|
fs::relpath q;
|
|
q.clear();
|
|
q.consolidate_to_rel(/*strip_leading_slash=*/true);
|
|
TEST_CHECK(q.empty());
|
|
|
|
// A non-root path (one prepend, then consolidate with strip) yields
|
|
// canonical-rel form: "name", no leading slash. Combined with the
|
|
// empty cases above, the dispatcher sees:
|
|
// nodeid == FUSE_ROOT_ID, empty() -> path-based op (root)
|
|
// nodeid != FUSE_ROOT_ID, empty() -> fh-based op (nameless)
|
|
// nodeid != FUSE_ROOT_ID, !empty() -> path-based op (named)
|
|
fs::relpath r;
|
|
r.clear();
|
|
r.prepend("file");
|
|
r.consolidate_to_rel(/*strip_leading_slash=*/true);
|
|
TEST_CHECK(!r.empty());
|
|
TEST_CHECK(r == "file");
|
|
}
|
|
|
|
void
|
|
test_rnd_rand64_basic()
|
|
{
|
|
u64 v1 = RND::rand64();
|
|
u64 v2 = RND::rand64();
|
|
TEST_CHECK(v1 != v2);
|
|
}
|
|
|
|
void
|
|
test_rnd_rand64_max()
|
|
{
|
|
for(int i = 0; i < 1000; ++i)
|
|
{
|
|
u64 v = RND::rand64(100);
|
|
TEST_CHECK(v < 100);
|
|
}
|
|
}
|
|
|
|
void
|
|
test_rnd_rand64_range()
|
|
{
|
|
for(int i = 0; i < 1000; ++i)
|
|
{
|
|
u64 v = RND::rand64(10, 50);
|
|
TEST_CHECK(v >= 10);
|
|
TEST_CHECK(v < 50);
|
|
}
|
|
}
|
|
|
|
void
|
|
test_rnd_shrink_to_rand_elem()
|
|
{
|
|
std::vector<int> v = {1};
|
|
RND::shrink_to_rand_elem(v);
|
|
TEST_CHECK(v.size() == 1);
|
|
|
|
std::vector<int> v2;
|
|
RND::shrink_to_rand_elem(v2);
|
|
TEST_CHECK(v2.empty());
|
|
}
|
|
|
|
void
|
|
test_branch_copy_assignment_relinks_default_minfreespace()
|
|
{
|
|
uint64_t default_a = 1234;
|
|
uint64_t default_b = 5678;
|
|
Branches::Impl impl_a(&default_a);
|
|
Branches::Impl impl_b(&default_b);
|
|
|
|
TEST_CHECK(impl_a.from_string("/a") == 0);
|
|
TEST_CHECK(impl_b.from_string("/b") == 0);
|
|
|
|
impl_a = impl_b;
|
|
|
|
TEST_CHECK(impl_a.size() == 1);
|
|
for(auto &branch : impl_a)
|
|
{
|
|
if(std::holds_alternative<const u64*>(branch._minfreespace))
|
|
TEST_CHECK(std::get<const u64*>(branch._minfreespace) == &default_a);
|
|
}
|
|
}
|
|
|
|
void
|
|
test_branch_move_assignment_relinks_default_minfreespace()
|
|
{
|
|
uint64_t default_a = 1234;
|
|
uint64_t default_b = 5678;
|
|
Branches::Impl impl_a(&default_a);
|
|
Branches::Impl impl_b(&default_b);
|
|
|
|
TEST_CHECK(impl_a.from_string("/a") == 0);
|
|
TEST_CHECK(impl_b.from_string("/b") == 0);
|
|
|
|
impl_a = std::move(impl_b);
|
|
|
|
TEST_CHECK(impl_a.size() == 1);
|
|
for(auto &branch : impl_a)
|
|
{
|
|
if(std::holds_alternative<const u64*>(branch._minfreespace))
|
|
TEST_CHECK(std::get<const u64*>(branch._minfreespace) == &default_a);
|
|
}
|
|
}
|
|
|
|
void
|
|
test_rapidhash_withSeed_preserves_default_output()
|
|
{
|
|
uint8_t buffer[192];
|
|
const uint64_t seeds[] = {
|
|
0,
|
|
1,
|
|
0x0123456789abcdefull,
|
|
0xffffffffffffffffull,
|
|
0xa0761d6478bd642full,
|
|
};
|
|
|
|
for(size_t i = 0; i < sizeof(buffer); ++i)
|
|
buffer[i] = (uint8_t)(((i * 131u) + 17u) & 0xffu);
|
|
|
|
for(size_t offset = 0; offset < 16; ++offset)
|
|
{
|
|
const uint8_t *key = buffer + offset;
|
|
|
|
for(size_t len = 0; len <= 160; ++len)
|
|
{
|
|
for(const uint64_t seed : seeds)
|
|
{
|
|
const uint64_t canonical =
|
|
rapidhash_internal(key,len,seed,rapid_secret);
|
|
|
|
TEST_CHECK(rapidhash_withSeed(key,len,seed) == canonical);
|
|
|
|
if(len <= 48)
|
|
TEST_CHECK(rapidhashNano_withSeed(key,len,seed) == canonical);
|
|
|
|
if(len <= 80)
|
|
TEST_CHECK(rapidhashMicro_withSeed(key,len,seed) == canonical);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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},
|
|
{"hashset_put_and_size",test_hashset_put_and_size},
|
|
{"rapidhash_withSeed_preserves_default_output",test_rapidhash_withSeed_preserves_default_output},
|
|
{"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},
|
|
{"fs_inode_readdir_calc_matches_calc",test_fs_inode_readdir_calc_matches_calc},
|
|
{"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_remember_nodes_bool_numeric",test_config_remember_nodes_bool_numeric},
|
|
{"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},
|
|
{"fs_clonepath_empty_relpath",test_fs_clonepath_empty_relpath},
|
|
{"fs_clonepath_single_component",test_fs_clonepath_single_component},
|
|
{"fs_clonepath_many_components",test_fs_clonepath_many_components},
|
|
{"fs_clonepath_intermediate_exists",test_fs_clonepath_intermediate_exists},
|
|
{"fs_clonepath_non_dir_intermediate",test_fs_clonepath_non_dir_intermediate},
|
|
{"fs_clonepath_lstat_failure",test_fs_clonepath_lstat_failure},
|
|
{"fs_clonepath_double_slash_skip",test_fs_clonepath_double_slash_skip},
|
|
{"str_eq_nullptr",test_str_eq_nullptr},
|
|
{"str_startswith_char_nullptr",test_str_startswith_char_nullptr},
|
|
{"str_from_u64_suffixes",test_str_from_u64_suffixes},
|
|
{"str_from_s64",test_str_from_s64},
|
|
{"str_from_int",test_str_from_int},
|
|
{"num_humanize",test_num_humanize},
|
|
{"rmdir_err_default_is_enoent",test_rmdir_err_default_is_enoent},
|
|
{"rmdir_err_first_success_sets_zero",test_rmdir_err_first_success_sets_zero},
|
|
{"rmdir_err_first_error_sets_value",test_rmdir_err_first_error_sets_value},
|
|
{"rmdir_err_enotempty_overwrites_other",test_rmdir_err_enotempty_overwrites_other},
|
|
{"rmdir_err_eexist_overwrites_other",test_rmdir_err_eexist_overwrites_other},
|
|
{"rmdir_err_enotempty_not_overwritten_by_other",test_rmdir_err_enotempty_not_overwritten_by_other},
|
|
{"rmdir_err_eexist_not_overwritten_by_other",test_rmdir_err_eexist_not_overwritten_by_other},
|
|
{"rmdir_err_success_does_not_overwrite_priority",test_rmdir_err_success_does_not_overwrite_priority},
|
|
{"rmdir_err_success_overwrites_generic_error",test_rmdir_err_success_overwrites_generic_error},
|
|
{"rmdir_err_enotempty_overwrites_enotempty",test_rmdir_err_enotempty_overwrites_enotempty},
|
|
{"hashset_inline_to_heap_growth",test_hashset_inline_to_heap_growth},
|
|
{"hashset_empty_string",test_hashset_empty_string},
|
|
{"hashset_many_items",test_hashset_many_items},
|
|
{"fs_inode_set_algo_valid",test_fs_inode_set_algo_valid},
|
|
{"fs_inode_set_algo_invalid",test_fs_inode_set_algo_invalid},
|
|
{"fs_inode_passthrough_returns_raw_ino",test_fs_inode_passthrough_returns_raw_ino},
|
|
{"fs_inode_different_algos_produce_distinct_inodes",test_fs_inode_different_algos_produce_distinct_inodes},
|
|
{"fs_path_default_construct",test_fs_relpath_default_construct},
|
|
{"fs_path_construct_from_strings",test_fs_relpath_construct_from_strings},
|
|
{"fs_path_concat_canonical",test_fs_relpath_concat_canonical},
|
|
{"fs_path_assign_concat_reuses_capacity",test_fs_relpath_assign_concat_reuses_capacity},
|
|
{"fs_path_operator_slash",test_fs_relpath_operator_slash},
|
|
{"fs_path_filename",test_fs_relpath_filename},
|
|
{"fs_path_parent_path",test_fs_relpath_parent_path},
|
|
{"fs_path_extension_and_stem",test_fs_relpath_extension_and_stem},
|
|
{"fs_path_remove_filename",test_fs_relpath_remove_filename},
|
|
{"fs_path_lexically_relative",test_fs_relpath_lexically_relative},
|
|
{"fs_path_copy_and_move",test_fs_relpath_copy_and_move},
|
|
{"fs_relpath_equality",test_fs_relpath_equality},
|
|
{"fs_relpath_implicit_conversions",test_fs_relpath_implicit_conversions},
|
|
{"fs_relpath_resize_and_append",test_fs_relpath_resize_and_append},
|
|
{"fs_relpath_construct_stores_verbatim",test_fs_relpath_construct_stores_verbatim},
|
|
{"fs_relpath_set_prefix",test_fs_relpath_set_prefix},
|
|
{"fs_relpath_concat",test_fs_relpath_concat},
|
|
{"fs_relpath_prepend",test_fs_relpath_prepend},
|
|
{"fs_relpath_consolidate_to_rel",test_fs_relpath_consolidate_to_rel},
|
|
{"fs_relpath_clear_resets_state",test_fs_relpath_clear_resets_state},
|
|
{"fs_relpath_self_aliasing",test_fs_relpath_self_aliasing},
|
|
{"fs_relpath_prefix_and_rel_accessors",test_fs_relpath_prefix_and_rel_accessors},
|
|
{"fs_relpath_resize_zero_fills",test_fs_relpath_resize_zero_fills},
|
|
{"fs_relpath_rel_size_invariant",test_fs_relpath_rel_size_invariant},
|
|
{"fs_relpath_swap",test_fs_relpath_swap},
|
|
{"fs_relpath_set_prefix_oversize",test_fs_relpath_set_prefix_oversize},
|
|
{"fs_relpath_root_nodeid_dispatch_contract",test_fs_relpath_root_nodeid_dispatch_contract},
|
|
{"rnd_rand64_basic",test_rnd_rand64_basic},
|
|
{"rnd_rand64_max",test_rnd_rand64_max},
|
|
{"rnd_rand64_range",test_rnd_rand64_range},
|
|
{"rnd_shrink_to_rand_elem",test_rnd_shrink_to_rand_elem},
|
|
{"branch_copy_assignment_relinks_default_minfreespace",test_branch_copy_assignment_relinks_default_minfreespace},
|
|
{"branch_move_assignment_relinks_default_minfreespace",test_branch_move_assignment_relinks_default_minfreespace},
|
|
{"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}
|
|
};
|