mirror of
https://github.com/trapexit/mergerfs.git
synced 2026-10-26 02:48:59 -04:00
Two gaps found while using this against a real media server. Some programs do very different jobs under one name. Plex runs credits detection with the same 'Plex Transcoder' binary that serves playback, reading the same file, so neither comm nor path separates them. Adds a cmdline match field, and quoting so a pattern can contain spaces -- without which 'Plex Transcoder' cannot be written down at all. Adds a 'critical' class flag for services playback synchronously waits on. Plex's EasyAudioEncoder is the case: a transcode of EAC3 audio writes to EAE and blocks. Such a helper looks like background work, so a governor protecting playback will throttle or suspend it and hang the stream waiting behind it. A critical class is never delayed. Also fixes qos.max-sleepers auto-sizing, which read the requested thread count rather than the resolved one and so capped at 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4294 lines
111 KiB
C++
4294 lines
111 KiB
C++
#include "acutest/acutest.h"
|
|
|
|
#include "config.hpp"
|
|
#include "fs_copyfile.hpp"
|
|
#include "fs_inode.hpp"
|
|
#include "from_string.hpp"
|
|
#include "hashset.hpp"
|
|
#include "num.hpp"
|
|
#include "rapidhash/rapidhash.h"
|
|
#include "rnd.hpp"
|
|
#include "qos_rules.hpp"
|
|
#include "str.hpp"
|
|
#include "thread_pool.hpp"
|
|
|
|
#include <atomic>
|
|
#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<fs::path> 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 fs::path branch_path("/mnt/disk1");
|
|
const std::vector<fs::path> dirpaths = {
|
|
fs::path(),
|
|
fs::path("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::path fullpath = dirpath / name;
|
|
const u64 expected = fs::inode::calc(branch_path.string(),
|
|
fullpath.string(),
|
|
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::path src_path;
|
|
fs::path dst_path;
|
|
fs::path tmp_dir;
|
|
char tmp_template[] = "/tmp/mergerfs-test-copyfile-basic-XXXXXX";
|
|
|
|
if(::mkdtemp(tmp_template) == nullptr)
|
|
{
|
|
TEST_CHECK(false);
|
|
return;
|
|
}
|
|
|
|
tmp_dir = tmp_template;
|
|
src_path = tmp_dir / "src.bin";
|
|
dst_path = tmp_dir / "dst.bin";
|
|
|
|
src_fd = ::open(src_path.c_str(),O_RDWR|O_CREAT|O_TRUNC,0600);
|
|
TEST_CHECK(src_fd >= 0);
|
|
if(src_fd < 0)
|
|
{
|
|
std::filesystem::remove_all(tmp_dir);
|
|
return;
|
|
}
|
|
|
|
rv = ::ftruncate(src_fd,(16 * 1024 * 1024));
|
|
TEST_CHECK(rv == 0);
|
|
rv = ::pwrite(src_fd,"A",1,0);
|
|
TEST_CHECK(rv == 1);
|
|
rv = ::pwrite(src_fd,"Z",1,((16 * 1024 * 1024) - 1));
|
|
TEST_CHECK(rv == 1);
|
|
|
|
rv = fs::copyfile(src_fd,dst_path,{.cleanup_failure = true});
|
|
TEST_CHECK(rv == 0);
|
|
|
|
rv = ::fstat(src_fd,&st);
|
|
TEST_CHECK(rv == 0);
|
|
|
|
struct stat dst_st;
|
|
rv = ::stat(dst_path.c_str(),&dst_st);
|
|
TEST_CHECK(rv == 0);
|
|
if(rv == 0)
|
|
TEST_CHECK(st.st_size == dst_st.st_size);
|
|
|
|
char c;
|
|
int dst_fd = ::open(dst_path.c_str(),O_RDONLY);
|
|
TEST_CHECK(dst_fd >= 0);
|
|
if(dst_fd >= 0)
|
|
{
|
|
rv = ::pread(dst_fd,&c,1,0);
|
|
TEST_CHECK((rv == 1) && (c == 'A'));
|
|
rv = ::pread(dst_fd,&c,1,(dst_st.st_size - 1));
|
|
TEST_CHECK((rv == 1) && (c == 'Z'));
|
|
::close(dst_fd);
|
|
}
|
|
|
|
::close(src_fd);
|
|
std::filesystem::remove_all(tmp_dir);
|
|
}
|
|
|
|
void
|
|
test_fs_copyfile_source_changes_cleanup_tmpfiles()
|
|
{
|
|
int rv;
|
|
int src_fd;
|
|
fs::path src_path;
|
|
fs::path dst_path;
|
|
fs::path tmp_dir;
|
|
std::atomic<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(tmp_dir);
|
|
return;
|
|
}
|
|
|
|
rv = ::ftruncate(src_fd,(64 * 1024 * 1024));
|
|
TEST_CHECK(rv == 0);
|
|
|
|
auto mutator = std::thread([&]()
|
|
{
|
|
for(int i = 0; i < 200 && !stop_mutator.load(); ++i)
|
|
{
|
|
struct timespec ts[2];
|
|
|
|
::clock_gettime(CLOCK_REALTIME,&ts[0]);
|
|
ts[1] = ts[0];
|
|
::utimensat(AT_FDCWD,src_path.c_str(),ts,0);
|
|
|
|
mutator_updates.fetch_add(1);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
});
|
|
|
|
rv = fs::copyfile(src_fd,dst_path,{.cleanup_failure = true});
|
|
stop_mutator.store(true);
|
|
mutator.join();
|
|
|
|
TEST_CHECK(rv == 0);
|
|
TEST_CHECK(mutator_updates.load() > 0);
|
|
|
|
bool found_tmp_file = false;
|
|
std::string tmp_prefix;
|
|
tmp_prefix = ".";
|
|
tmp_prefix += dst_path.filename().string();
|
|
tmp_prefix += "_";
|
|
for(const auto &entry : std::filesystem::directory_iterator(tmp_dir))
|
|
{
|
|
const std::string name = entry.path().filename().string();
|
|
if(name.rfind(tmp_prefix,0) == 0)
|
|
{
|
|
found_tmp_file = true;
|
|
break;
|
|
}
|
|
}
|
|
TEST_CHECK(found_tmp_file == false);
|
|
|
|
::close(src_fd);
|
|
std::filesystem::remove_all(tmp_dir);
|
|
}
|
|
|
|
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 fs::path branch("/mnt/disk1");
|
|
const fs::path 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 fs::path branch("/mnt/disk1");
|
|
const fs::path 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_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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
QoS ruleset parsing.
|
|
|
|
The parser is the part of the QoS engine a user actually touches, and
|
|
a ruleset that parses "successfully" into the wrong thing would
|
|
silently misprioritise every request. These check both that valid
|
|
input produces the expected classification and that invalid input is
|
|
rejected rather than partially applied.
|
|
*/
|
|
|
|
static
|
|
qos::RuleSet::Ptr
|
|
qos_parse(const char *text_)
|
|
{
|
|
std::string err;
|
|
|
|
return qos::RuleSet::parse(text_,&err);
|
|
}
|
|
|
|
// Owns the strings a Subject points at. Building a Subject from
|
|
// temporaries leaves it holding dangling pointers the moment the full
|
|
// expression ends.
|
|
struct QoSSubj
|
|
{
|
|
std::string cgroup;
|
|
std::string comm;
|
|
std::string path;
|
|
std::string cmdline;
|
|
qos::Subject s;
|
|
|
|
QoSSubj(std::string cgroup_,
|
|
std::string comm_,
|
|
std::string path_,
|
|
const u32 uid_ = 0,
|
|
const qos::Direction dir_ = qos::Direction::READ,
|
|
std::string cmdline_ = {})
|
|
: cgroup(std::move(cgroup_)),
|
|
comm(std::move(comm_)),
|
|
path(std::move(path_)),
|
|
cmdline(std::move(cmdline_))
|
|
{
|
|
s.cgroup = &cgroup;
|
|
s.comm = &comm;
|
|
s.path = &path;
|
|
s.cmdline = &cmdline;
|
|
s.uid = uid_;
|
|
s.gid = 0;
|
|
s.dir = dir_;
|
|
}
|
|
};
|
|
|
|
static
|
|
void
|
|
test_qos_parse_size(void)
|
|
{
|
|
u64 v;
|
|
|
|
TEST_CHECK(qos::parse_size("1024",&v) == 0 && v == 1024);
|
|
TEST_CHECK(qos::parse_size("1K",&v) == 0 && v == 1024);
|
|
TEST_CHECK(qos::parse_size("1KiB",&v) == 0 && v == 1024);
|
|
TEST_CHECK(qos::parse_size("1MB",&v) == 0 && v == 1024 * 1024);
|
|
TEST_CHECK(qos::parse_size("1.5M",&v) == 0 && v == (1024 * 1024 * 3) / 2);
|
|
TEST_CHECK(qos::parse_size("2G",&v) == 0 && v == 2ULL * 1024 * 1024 * 1024);
|
|
|
|
TEST_CHECK(qos::parse_size("",&v) != 0);
|
|
TEST_CHECK(qos::parse_size("M",&v) != 0);
|
|
TEST_CHECK(qos::parse_size("10Q",&v) != 0);
|
|
TEST_CHECK(qos::parse_size("abc",&v) != 0);
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_parse_ioprio(void)
|
|
{
|
|
int v;
|
|
|
|
TEST_CHECK(qos::ioprio::from_string("idle",&v) == 0);
|
|
TEST_CHECK(qos::ioprio::to_string(v) == "idle");
|
|
|
|
TEST_CHECK(qos::ioprio::from_string("rt:0",&v) == 0);
|
|
TEST_CHECK(qos::ioprio::to_string(v) == "rt:0");
|
|
|
|
TEST_CHECK(qos::ioprio::from_string("be:7",&v) == 0);
|
|
TEST_CHECK(qos::ioprio::to_string(v) == "be:7");
|
|
|
|
TEST_CHECK(qos::ioprio::from_string("be:8",&v) != 0);
|
|
TEST_CHECK(qos::ioprio::from_string("be:",&v) != 0);
|
|
TEST_CHECK(qos::ioprio::from_string("zz:1",&v) != 0);
|
|
TEST_CHECK(qos::ioprio::from_string("",&v) != 0);
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_rules_basic(void)
|
|
{
|
|
auto rs = qos_parse("class fast ioprio=rt:0\n"
|
|
"class slow ioprio=idle nice=19 rate=8M\n"
|
|
"match comm = rsync -> slow\n"
|
|
"default fast\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
TEST_CHECK(!rs->inert());
|
|
TEST_CHECK(rs->needs_comm());
|
|
TEST_CHECK(!rs->needs_cgroup());
|
|
TEST_CHECK(!rs->needs_path());
|
|
|
|
QoSSubj rsync("","rsync","/a");
|
|
TEST_CHECK(rs->classify(rsync.s)->name == "slow");
|
|
|
|
QoSSubj cat("","cat","/a");
|
|
TEST_CHECK(rs->classify(cat.s)->name == "fast");
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_rules_multi_condition(void)
|
|
{
|
|
// Every condition must hold. This is what makes "this program, but
|
|
// only on this folder" expressible as a single rule.
|
|
auto rs = qos_parse("class bulk ioprio=idle\n"
|
|
"class norm ioprio=be:4\n"
|
|
"match cgroup ~ *lxc/3111* comm ~ *Butler* -> bulk\n"
|
|
"default norm\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
TEST_CHECK(rs->needs_cgroup());
|
|
TEST_CHECK(rs->needs_comm());
|
|
|
|
QoSSubj both("0::/lxc/3111/init","PlexButler","/x");
|
|
TEST_CHECK(rs->classify(both.s)->name == "bulk");
|
|
|
|
// Right container, wrong program.
|
|
QoSSubj one("0::/lxc/3111/init","PlexTranscoder","/x");
|
|
TEST_CHECK(rs->classify(one.s)->name == "norm");
|
|
|
|
// Right program, wrong container.
|
|
QoSSubj other("0::/lxc/3129/init","PlexButler","/x");
|
|
TEST_CHECK(rs->classify(other.s)->name == "norm");
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_rules_path_and_op(void)
|
|
{
|
|
auto rs = qos_parse("class slow ioprio=idle rate=1M\n"
|
|
"class norm ioprio=be:4\n"
|
|
"match path ~ /TV/* op = write -> slow\n"
|
|
"default norm\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
TEST_CHECK(rs->needs_path());
|
|
|
|
QoSSubj w("","x","/TV/show/ep.mkv",0,qos::Direction::WRITE);
|
|
TEST_CHECK(rs->classify(w.s)->name == "slow");
|
|
|
|
// Same path, but a read.
|
|
QoSSubj r("","x","/TV/show/ep.mkv",0,qos::Direction::READ);
|
|
TEST_CHECK(rs->classify(r.s)->name == "norm");
|
|
|
|
// A write, but elsewhere. Globs are matched without FNM_PATHNAME so
|
|
// '*' spans separators.
|
|
QoSSubj e("","x","/Movies/a.mkv",0,qos::Direction::WRITE);
|
|
TEST_CHECK(rs->classify(e.s)->name == "norm");
|
|
|
|
QoSSubj deep("","x","/TV/a/b/c.mkv",0,qos::Direction::WRITE);
|
|
TEST_CHECK(rs->classify(deep.s)->name == "slow");
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_rules_first_match_wins(void)
|
|
{
|
|
auto rs = qos_parse("class a ioprio=rt:0\n"
|
|
"class b ioprio=idle\n"
|
|
"match comm ~ pl* -> a\n"
|
|
"match comm ~ plex -> b\n"
|
|
"default b\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
|
|
QoSSubj plex("","plex","/x");
|
|
TEST_CHECK(rs->classify(plex.s)->name == "a");
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_rules_capacity_and_percent(void)
|
|
{
|
|
auto rs = qos_parse("capacity /mnt/disk1 100M\n"
|
|
"capacity default 50M\n"
|
|
"class pct rate=10%\n"
|
|
"class abs rate=4M\n"
|
|
"match uid = 1000 -> pct\n"
|
|
"default abs\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
|
|
const qos::Class *pct = rs->find_class("pct");
|
|
const qos::Class *abs = rs->find_class("abs");
|
|
TEST_ASSERT(pct && abs);
|
|
|
|
TEST_CHECK(rs->capacity("/mnt/disk1") == 100 * 1024 * 1024);
|
|
// Unknown resources fall back to the declared default.
|
|
TEST_CHECK(rs->capacity("/mnt/nope") == 50 * 1024 * 1024);
|
|
|
|
TEST_CHECK(rs->rate_for(pct,"/mnt/disk1") == (100 * 1024 * 1024) / 10);
|
|
TEST_CHECK(rs->rate_for(pct,"/mnt/nope") == (50 * 1024 * 1024) / 10);
|
|
|
|
// An absolute rate ignores the resource entirely.
|
|
TEST_CHECK(rs->rate_for(abs,"/mnt/disk1") == 4 * 1024 * 1024);
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_percent_without_capacity_is_unlimited(void)
|
|
{
|
|
// Nothing declared a capacity, so 10% of an unknown number cannot be
|
|
// enforced. Running unlimited is the only honest reading; throttling
|
|
// against a guess would be worse.
|
|
auto rs = qos_parse("class pct rate=10%\n"
|
|
"default pct\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
|
|
const qos::Class *pct = rs->find_class("pct");
|
|
TEST_ASSERT(pct != nullptr);
|
|
TEST_CHECK(pct->limited());
|
|
TEST_CHECK(rs->rate_for(pct,"/mnt/whatever") == 0);
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_rules_errors(void)
|
|
{
|
|
std::string err;
|
|
|
|
// A rule may not name a class that does not exist yet.
|
|
TEST_CHECK(qos::RuleSet::parse("match comm = x -> nope\n",&err) == nullptr);
|
|
TEST_CHECK(!err.empty());
|
|
|
|
TEST_CHECK(qos::RuleSet::parse("class a ioprio=be:9\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a rate=10Q\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a nice=99\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a rate=0%\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a rate=101%\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a\nclass a\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a\nmatch bogus = x -> a\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a\nmatch comm ! x -> a\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a\nmatch comm = x a\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a\nmatch -> a\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a\ndefault nope\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("frobnicate\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a\nmatch uid = abc -> a\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a\nmatch uid ~ 100 -> a\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("class a\nmatch op = sideways -> a\n",&err) == nullptr);
|
|
TEST_CHECK(qos::RuleSet::parse("capacity default\n",&err) == nullptr);
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_rules_comments_and_blanks(void)
|
|
{
|
|
auto rs = qos_parse("# leading comment\n"
|
|
"\n"
|
|
"class a ioprio=rt:0 # trailing comment\n"
|
|
" \n"
|
|
"match comm = x -> a # another\n"
|
|
"default a\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
|
|
QoSSubj sub("","x","/p");
|
|
TEST_CHECK(rs->classify(sub.s)->name == "a");
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_empty_ruleset_is_inert(void)
|
|
{
|
|
// An empty or do-nothing ruleset must report itself inert so the hot
|
|
// path can skip reading /proc entirely.
|
|
auto rs = qos_parse("");
|
|
TEST_ASSERT(rs != nullptr);
|
|
TEST_CHECK(rs->inert());
|
|
|
|
auto named = qos_parse("class a\ndefault a\n");
|
|
TEST_ASSERT(named != nullptr);
|
|
TEST_CHECK(named->inert());
|
|
|
|
auto live = qos_parse("class a ioprio=idle\ndefault a\n");
|
|
TEST_ASSERT(live != nullptr);
|
|
TEST_CHECK(!live->inert());
|
|
|
|
auto dflt = qos::RuleSet::make_default();
|
|
TEST_ASSERT(dflt != nullptr);
|
|
TEST_CHECK(dflt->inert());
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_buckets_are_per_resource(void)
|
|
{
|
|
// A rate is a per-device allowance. Two branches must not share one
|
|
// bucket, or a class limited to 10% would be limited to 10% of one
|
|
// disk spread across all of them.
|
|
auto rs = qos_parse("class a rate=1M\ndefault a\n");
|
|
TEST_ASSERT(rs != nullptr);
|
|
|
|
const qos::Class *a = rs->find_class("a");
|
|
TEST_ASSERT(a != nullptr);
|
|
|
|
qos::Bucket *b1 = a->bucket_for("/mnt/disk1");
|
|
qos::Bucket *b2 = a->bucket_for("/mnt/disk2");
|
|
|
|
TEST_CHECK(b1 != nullptr);
|
|
TEST_CHECK(b2 != nullptr);
|
|
TEST_CHECK(b1 != b2);
|
|
// Asking again returns the same bucket rather than a fresh one.
|
|
TEST_CHECK(a->bucket_for("/mnt/disk1") == b1);
|
|
}
|
|
|
|
|
|
|
|
static
|
|
void
|
|
test_qos_rules_cmdline(void)
|
|
{
|
|
// The case this field exists for: Plex runs credits detection under
|
|
// the *same* `Plex Transcoder` binary as real playback, reading the
|
|
// same media file. Neither comm nor path can separate them -- only
|
|
// the command line, where a detection job writes its output into
|
|
// .../Transcode/Detection/ instead of feeding a session.
|
|
auto rs = qos_parse("class playback protect\n"
|
|
"class analysis yield=100 floor=5% ioprio=idle\n"
|
|
"match comm = 'Plex Transcoder' cmdline ~ */Transcode/Detection/* -> analysis\n"
|
|
"match comm = 'Plex Transcoder' -> playback\n"
|
|
"default playback\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
TEST_CHECK(rs->needs_cmdline());
|
|
|
|
QoSSubj detect("","Plex Transcoder","/TV/ep.mkv",0,qos::Direction::READ,
|
|
"/usr/lib/plexmediaserver/Plex Transcoder -i /storage/TV/ep.mkv "
|
|
"-f flac /transcode/Transcode/Detection/abc123");
|
|
TEST_CHECK(rs->classify(detect.s)->name == "analysis");
|
|
|
|
// Same binary, same file, but feeding a session.
|
|
QoSSubj play("","Plex Transcoder","/TV/ep.mkv",0,qos::Direction::READ,
|
|
"/usr/lib/plexmediaserver/Plex Transcoder -i /storage/TV/ep.mkv "
|
|
"-f hls /transcode/Transcode/Sessions/xyz789");
|
|
TEST_CHECK(rs->classify(play.s)->name == "playback");
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_cmdline_not_read_when_unused(void)
|
|
{
|
|
// Reading /proc/<pid>/cmdline is only worth it when some rule looks
|
|
// at it.
|
|
auto rs = qos_parse("class a ioprio=idle\n"
|
|
"match comm = x -> a\n"
|
|
"default a\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
TEST_CHECK(!rs->needs_cmdline());
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_critical_is_never_throttled(void)
|
|
{
|
|
// A class playback synchronously waits on -- Plex's EasyAudioEncoder
|
|
// is the real-world case -- must never be delayed, even if the
|
|
// ruleset gives it a rate. Throttling it throttles the stream
|
|
// blocked behind it.
|
|
auto rs = qos_parse("class eae critical rate=1M\n"
|
|
"class bulk rate=1M\n"
|
|
"match comm = EasyAudioEncode -> eae\n"
|
|
"default bulk\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
|
|
const qos::Class *eae = rs->find_class("eae");
|
|
const qos::Class *bulk = rs->find_class("bulk");
|
|
TEST_ASSERT(eae && bulk);
|
|
|
|
TEST_CHECK(eae->critical);
|
|
TEST_CHECK(eae->immune());
|
|
TEST_CHECK(!bulk->immune());
|
|
|
|
QoSSubj s("","EasyAudioEncode","/x");
|
|
TEST_CHECK(rs->classify(s.s)->name == "eae");
|
|
}
|
|
|
|
static
|
|
void
|
|
test_qos_protect_implies_immune(void)
|
|
{
|
|
auto rs = qos_parse("class p protect\nclass b yield=100\ndefault b\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
|
|
TEST_CHECK(rs->find_class("p")->immune());
|
|
TEST_CHECK(!rs->find_class("b")->immune());
|
|
TEST_CHECK(rs->has_protected());
|
|
}
|
|
|
|
|
|
|
|
static
|
|
void
|
|
test_qos_quoted_patterns(void)
|
|
{
|
|
// Without quoting, none of the process names that actually matter
|
|
// here can be written down at all.
|
|
auto rs = qos_parse("class a ioprio=idle\n"
|
|
"class b ioprio=be:4\n"
|
|
"match comm = \"Plex Media Scanner\" -> a\n"
|
|
"default b\n");
|
|
|
|
TEST_ASSERT(rs != nullptr);
|
|
|
|
QoSSubj scanner("","Plex Media Scanner","/x");
|
|
TEST_CHECK(rs->classify(scanner.s)->name == "a");
|
|
|
|
QoSSubj other("","Plex","/x");
|
|
TEST_CHECK(rs->classify(other.s)->name == "b");
|
|
|
|
// Single quotes work the same way, and a glob may be quoted too.
|
|
auto rs2 = qos_parse("class a ioprio=idle\n"
|
|
"class b ioprio=be:4\n"
|
|
"match path ~ '/TV/The Bear/*' -> a\n"
|
|
"default b\n");
|
|
TEST_ASSERT(rs2 != nullptr);
|
|
|
|
QoSSubj bear("","x","/TV/The Bear/S01E01.mkv");
|
|
TEST_CHECK(rs2->classify(bear.s)->name == "a");
|
|
|
|
QoSSubj notbear("","x","/TV/Other/S01E01.mkv");
|
|
TEST_CHECK(rs2->classify(notbear.s)->name == "b");
|
|
}
|
|
|
|
|
|
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},
|
|
{"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},
|
|
{"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},
|
|
{"qos_parse_size",test_qos_parse_size},
|
|
{"qos_parse_ioprio",test_qos_parse_ioprio},
|
|
{"qos_rules_basic",test_qos_rules_basic},
|
|
{"qos_rules_multi_condition",test_qos_rules_multi_condition},
|
|
{"qos_rules_path_and_op",test_qos_rules_path_and_op},
|
|
{"qos_rules_first_match_wins",test_qos_rules_first_match_wins},
|
|
{"qos_rules_capacity_and_percent",test_qos_rules_capacity_and_percent},
|
|
{"qos_percent_without_capacity_is_unlimited",test_qos_percent_without_capacity_is_unlimited},
|
|
{"qos_rules_errors",test_qos_rules_errors},
|
|
{"qos_rules_comments_and_blanks",test_qos_rules_comments_and_blanks},
|
|
{"qos_empty_ruleset_is_inert",test_qos_empty_ruleset_is_inert},
|
|
{"qos_buckets_are_per_resource",test_qos_buckets_are_per_resource},
|
|
{"qos_quoted_patterns",test_qos_quoted_patterns},
|
|
{"qos_rules_cmdline",test_qos_rules_cmdline},
|
|
{"qos_cmdline_not_read_when_unused",test_qos_cmdline_not_read_when_unused},
|
|
{"qos_critical_is_never_throttled",test_qos_critical_is_never_throttled},
|
|
{"qos_protect_implies_immune",test_qos_protect_implies_immune},
|
|
{NULL,NULL}
|
|
};
|