mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-28 11:43:50 +00:00
Add C++20 ownership and allocation core
Header-only primitives for the no-heap-after-startup design: Result/Status, Region, ObjectPool with PoolPtr (std::unique_ptr with a pool deleter), generation-checked Handle and asynchronous close support, intrusive RefPtr, FixedBuffer, MonotonicArena, FixedHashMap and FixedLru, IntrusiveList, and a bounded FixedHeap for third-party allocator hooks. Unit tests cover each primitive (test_core_*). scripts/check_no_heap.py rejects heap allocation in C++ sources under src/ and runs as a ctest. clang-tidy now also analyses C++ sources and the core headers.
This commit is contained in:
@@ -57,6 +57,19 @@ function(ss_add_test name sources libs)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# C++ core (src/core): header-only ownership and allocation primitives.
|
||||
foreach(core_test result pool buffer arena fixed_map ownership)
|
||||
ss_add_test(test_core_${core_test} "test_core_${core_test}.cpp" "")
|
||||
endforeach()
|
||||
|
||||
# C++ sources under src/ must not allocate on the heap (see the script).
|
||||
find_package(Python3 COMPONENTS Interpreter QUIET)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME check_no_heap
|
||||
COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/scripts/check_no_heap.py --self-test)
|
||||
set_tests_properties(check_no_heap PROPERTIES LABELS "unit")
|
||||
endif()
|
||||
|
||||
# test_base64 - no external deps
|
||||
ss_add_test(test_base64
|
||||
"test_base64.c;${PROJECT_SOURCE_DIR}/src/base64.c"
|
||||
@@ -215,6 +228,15 @@ ss_add_test(test_resolv_async
|
||||
"ss_ev;ss_cares;ss_sodium")
|
||||
set_tests_properties(test_resolv_async PROPERTIES TIMEOUT 30)
|
||||
|
||||
# Preloadable malloc counter for the allocation audit. It interposes the
|
||||
# system allocator, so it is only built where that is supported (glibc,
|
||||
# macOS) and never together with the sanitizers, which own malloc.
|
||||
include(CheckSymbolExists)
|
||||
check_symbol_exists(__GLIBC__ "stdlib.h" SS_HAVE_GLIBC)
|
||||
if(SS_BUILD_EXECUTABLES AND NOT ENABLE_SANITIZERS AND (APPLE OR SS_HAVE_GLIBC))
|
||||
add_library(ss_alloc_audit SHARED alloc_audit/alloc_counter.c)
|
||||
endif()
|
||||
|
||||
if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
|
||||
find_package(Python3 COMPONENTS Interpreter QUIET)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
@@ -233,6 +255,15 @@ if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/experiments/test_transport.py)
|
||||
set_tests_properties(test_experimental_transports PROPERTIES
|
||||
LABELS "integration" TIMEOUT 30)
|
||||
# Steady-state allocation audit (report-only until the C++ relay
|
||||
# lands; see tests/alloc_audit/measure_alloc.py).
|
||||
if(TARGET ss_alloc_audit)
|
||||
add_test(NAME alloc_audit_report
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/measure_alloc.py
|
||||
--bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
|
||||
--shim $<TARGET_FILE:ss_alloc_audit>)
|
||||
set_tests_properties(alloc_audit_report PROPERTIES LABELS "integration" TIMEOUT 120)
|
||||
endif()
|
||||
add_test(NAME test_printable_salt_interop
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/interop.py
|
||||
--self --printable-salt --bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "core/arena.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
struct Pair {
|
||||
int a;
|
||||
double b;
|
||||
};
|
||||
|
||||
void test_allocate_alignment()
|
||||
{
|
||||
ss::MonotonicArena arena;
|
||||
SS_CHECK(arena.reserve(256));
|
||||
auto c = arena.allocate(1, 1);
|
||||
SS_CHECK(c);
|
||||
auto d = arena.allocate(8, 64);
|
||||
SS_CHECK(d);
|
||||
SS_CHECK(reinterpret_cast<std::uintptr_t>(d->data()) % 64 == 0);
|
||||
auto p = arena.make<Pair>(Pair{ 3, 1.5 });
|
||||
SS_CHECK(p && (*p)->a == 3 && (*p)->b == 1.5);
|
||||
SS_CHECK(!arena.allocate(8, 3));
|
||||
}
|
||||
|
||||
void test_exhaustion_and_reset()
|
||||
{
|
||||
ss::MonotonicArena arena;
|
||||
SS_CHECK(!arena.allocate(1));
|
||||
SS_CHECK(arena.reserve(64));
|
||||
SS_CHECK(arena.allocate(64, 1));
|
||||
auto over = arena.allocate(1, 1);
|
||||
SS_CHECK(!over && over.error().code == ss::Errc::exhausted);
|
||||
arena.reset();
|
||||
SS_CHECK(arena.used() == 0 && arena.peak() == 64);
|
||||
SS_CHECK(arena.allocate(32, 1));
|
||||
}
|
||||
|
||||
void test_intern_and_arrays()
|
||||
{
|
||||
ss::MonotonicArena arena;
|
||||
SS_CHECK(arena.reserve(128));
|
||||
auto s = arena.intern("aes-256-gcm");
|
||||
SS_CHECK(s && *s == "aes-256-gcm");
|
||||
SS_CHECK(s->data()[s->size()] == '\0');
|
||||
auto empty = arena.intern("");
|
||||
SS_CHECK(empty && empty->empty());
|
||||
auto ports = arena.make_array<std::uint16_t>(4);
|
||||
SS_CHECK(ports && ports->size() == 4 && (*ports)[3] == 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_allocate_alignment);
|
||||
SS_RUN(test_exhaustion_and_reset);
|
||||
SS_RUN(test_intern_and_arrays);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/buffer.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
ss::ConstBytes bytes(std::string_view s)
|
||||
{
|
||||
return ss::as_bytes(s.data(), s.size());
|
||||
}
|
||||
|
||||
bool equals(ss::ConstBytes b, std::string_view s)
|
||||
{
|
||||
return b.size() == s.size() && std::memcmp(b.data(), s.data(), s.size()) == 0;
|
||||
}
|
||||
|
||||
void test_append_consume()
|
||||
{
|
||||
ss::FixedBuffer<8> buf;
|
||||
SS_CHECK(buf.empty() && buf.capacity() == 8);
|
||||
SS_CHECK(buf.append(bytes("hello")));
|
||||
SS_CHECK(equals(buf.readable(), "hello"));
|
||||
SS_CHECK(buf.consume(2));
|
||||
SS_CHECK(equals(buf.readable(), "llo"));
|
||||
SS_CHECK(!buf.consume(4));
|
||||
// Appending past the tail compacts first.
|
||||
SS_CHECK(buf.append(bytes("world")));
|
||||
SS_CHECK(equals(buf.readable(), "lloworld"));
|
||||
SS_CHECK(!buf.append(bytes("x")));
|
||||
SS_CHECK(buf.consume(8) && buf.empty());
|
||||
}
|
||||
|
||||
void test_commit_writable()
|
||||
{
|
||||
ss::FixedBuffer<4> buf;
|
||||
auto w = buf.writable();
|
||||
SS_CHECK(w.size() == 4);
|
||||
std::memcpy(w.data(), "abcd", 4);
|
||||
SS_CHECK(buf.commit(4));
|
||||
SS_CHECK(buf.full() && buf.writable().empty());
|
||||
SS_CHECK(!buf.commit(1));
|
||||
SS_CHECK(equals(buf.readable(), "abcd"));
|
||||
}
|
||||
|
||||
void test_prepend()
|
||||
{
|
||||
ss::FixedBuffer<10> buf;
|
||||
SS_CHECK(buf.append(bytes("data")));
|
||||
// No headroom: payload shifts right.
|
||||
SS_CHECK(buf.prepend(bytes("hdr")));
|
||||
SS_CHECK(equals(buf.readable(), "hdrdata"));
|
||||
SS_CHECK(buf.consume(3));
|
||||
// Headroom from the consumed bytes is reused in place.
|
||||
SS_CHECK(buf.prepend(bytes("ab")));
|
||||
SS_CHECK(equals(buf.readable(), "abdata"));
|
||||
SS_CHECK(!buf.prepend(bytes("12345")));
|
||||
SS_CHECK(equals(buf.readable(), "abdata"));
|
||||
}
|
||||
|
||||
void test_assign_wipe()
|
||||
{
|
||||
ss::FixedBuffer<4> buf;
|
||||
SS_CHECK(!buf.assign(bytes("toolong")));
|
||||
SS_CHECK(buf.assign(bytes("key")));
|
||||
SS_CHECK(equals(buf.readable(), "key"));
|
||||
buf.wipe();
|
||||
SS_CHECK(buf.empty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_append_consume);
|
||||
SS_RUN(test_commit_writable);
|
||||
SS_RUN(test_prepend);
|
||||
SS_RUN(test_assign_wipe);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/fixed_map.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using Key = ss::FixedKey<16>;
|
||||
|
||||
Key key(std::string_view s)
|
||||
{
|
||||
auto k = Key::from(ss::as_bytes(s.data(), s.size()));
|
||||
SS_CHECK(k.has_value());
|
||||
return *k;
|
||||
}
|
||||
|
||||
// Every key lands in the same home slot, to exercise probe runs and the
|
||||
// backward-shift deletion.
|
||||
struct CollidingHash {
|
||||
std::uint64_t operator()(std::uint32_t, std::uint64_t) const noexcept { return 7; }
|
||||
};
|
||||
|
||||
void test_map_basic()
|
||||
{
|
||||
ss::FixedHashMap<std::uint32_t, int> map;
|
||||
SS_CHECK(map.reserve(3, 0x1234));
|
||||
SS_CHECK(map.emplace(1, 10));
|
||||
SS_CHECK(map.emplace(2, 20));
|
||||
SS_CHECK(map.emplace(3, 30));
|
||||
auto dup = map.emplace(2, 99);
|
||||
SS_CHECK(!dup && dup.error().code == ss::Errc::invalid);
|
||||
auto over = map.emplace(4, 40);
|
||||
SS_CHECK(!over && over.error().code == ss::Errc::exhausted);
|
||||
SS_CHECK(*map.find(2) == 20);
|
||||
auto taken = map.take(2);
|
||||
SS_CHECK(taken && *taken == 20);
|
||||
SS_CHECK(!map.find(2) && map.size() == 2);
|
||||
SS_CHECK(map.emplace(4, 40));
|
||||
SS_CHECK(!map.erase(5));
|
||||
}
|
||||
|
||||
void test_map_backward_shift()
|
||||
{
|
||||
ss::FixedHashMap<std::uint32_t, int, CollidingHash> map;
|
||||
SS_CHECK(map.reserve(8));
|
||||
for (std::uint32_t i = 0; i < 8; ++i) {
|
||||
SS_CHECK(map.emplace(i, static_cast<int>(i) * 100));
|
||||
}
|
||||
// Remove from the middle of the probe run; later entries must still be
|
||||
// reachable without tombstones.
|
||||
SS_CHECK(map.erase(3));
|
||||
SS_CHECK(map.erase(0));
|
||||
for (std::uint32_t i = 0; i < 8; ++i) {
|
||||
const int *v = map.find(i);
|
||||
if (i == 0 || i == 3) {
|
||||
SS_CHECK(v == nullptr);
|
||||
} else {
|
||||
SS_CHECK(v != nullptr && *v == static_cast<int>(i) * 100);
|
||||
}
|
||||
}
|
||||
int count = 0;
|
||||
map.for_each([&](std::uint32_t, int &) { ++count; });
|
||||
SS_CHECK(count == 6);
|
||||
}
|
||||
|
||||
void test_map_move_only_values()
|
||||
{
|
||||
ss::FixedHashMap<std::uint32_t, std::unique_ptr<int> > map;
|
||||
SS_CHECK(map.reserve(2));
|
||||
SS_CHECK(map.emplace(1, std::make_unique<int>(5)));
|
||||
auto v = map.take(1);
|
||||
SS_CHECK(v && **v == 5);
|
||||
}
|
||||
|
||||
void test_lru_order()
|
||||
{
|
||||
ss::FixedLru<Key, int> lru;
|
||||
SS_CHECK(lru.reserve(3, 42));
|
||||
SS_CHECK(lru.emplace(key("a"), 1));
|
||||
SS_CHECK(lru.emplace(key("b"), 2));
|
||||
SS_CHECK(lru.emplace(key("c"), 3));
|
||||
SS_CHECK(lru.full());
|
||||
auto over = lru.emplace(key("d"), 4);
|
||||
SS_CHECK(!over && over.error().code == ss::Errc::exhausted);
|
||||
|
||||
// Touch "a" so "b" becomes the oldest.
|
||||
SS_CHECK(*lru.find(key("a")) == 1);
|
||||
SS_CHECK(*lru.oldest_key() == key("b"));
|
||||
// peek() does not change the order.
|
||||
SS_CHECK(*lru.peek(key("b")) == 2);
|
||||
SS_CHECK(*lru.oldest_key() == key("b"));
|
||||
|
||||
auto evicted = lru.pop_oldest();
|
||||
SS_CHECK(evicted && evicted->first == key("b") && evicted->second == 2);
|
||||
SS_CHECK(lru.emplace(key("d"), 4));
|
||||
|
||||
int order[3] = {};
|
||||
int n = 0;
|
||||
lru.for_each([&](const Key &, int &v) { order[n++] = v; });
|
||||
SS_CHECK(n == 3 && order[0] == 3 && order[1] == 1 && order[2] == 4);
|
||||
}
|
||||
|
||||
void test_lru_stable_values()
|
||||
{
|
||||
ss::FixedLru<std::uint32_t, int> lru;
|
||||
SS_CHECK(lru.reserve(64));
|
||||
auto first = lru.emplace(0, 1000);
|
||||
SS_CHECK(first);
|
||||
int *stable = *first;
|
||||
for (std::uint32_t i = 1; i < 64; ++i) {
|
||||
SS_CHECK(lru.emplace(i, static_cast<int>(i)));
|
||||
}
|
||||
for (std::uint32_t i = 1; i < 64; i += 2) {
|
||||
SS_CHECK(lru.erase(i));
|
||||
}
|
||||
// Erasing others reshuffles the index but never moves values.
|
||||
SS_CHECK(lru.peek(0) == stable && *stable == 1000);
|
||||
auto taken = lru.take(0);
|
||||
SS_CHECK(taken && *taken == 1000);
|
||||
SS_CHECK(lru.size() == 31);
|
||||
lru.clear();
|
||||
SS_CHECK(lru.empty() && lru.oldest() == nullptr);
|
||||
}
|
||||
|
||||
void test_key_limits()
|
||||
{
|
||||
char big[17] = {};
|
||||
SS_CHECK(!Key::from(ss::as_bytes(big, sizeof(big))));
|
||||
SS_CHECK(key("ab") != key("abc"));
|
||||
SS_CHECK(key("") == key(""));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_map_basic);
|
||||
SS_RUN(test_map_backward_shift);
|
||||
SS_RUN(test_map_move_only_values);
|
||||
SS_RUN(test_lru_order);
|
||||
SS_RUN(test_lru_stable_values);
|
||||
SS_RUN(test_key_limits);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
// IntrusiveList, RefPtr and FixedHeap: the remaining ownership primitives.
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#include "core/fixed_heap.hpp"
|
||||
#include "core/intrusive_list.hpp"
|
||||
#include "core/pool.hpp"
|
||||
#include "core/ref.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
struct IdleTag;
|
||||
|
||||
struct Conn : ss::ListNode<>, ss::ListNode<IdleTag> {
|
||||
explicit Conn(int v) noexcept : id(v) {}
|
||||
int id;
|
||||
};
|
||||
|
||||
void test_list_basic()
|
||||
{
|
||||
ss::IntrusiveList<Conn> all;
|
||||
ss::IntrusiveList<Conn, IdleTag> idle;
|
||||
Conn a(1), b(2), c(3);
|
||||
all.push_back(a);
|
||||
all.push_back(b);
|
||||
all.push_front(c);
|
||||
idle.push_back(b);
|
||||
SS_CHECK(all.size() == 3 && idle.size() == 1);
|
||||
SS_CHECK(all.front()->id == 3 && all.back()->id == 2);
|
||||
|
||||
all.move_to_back(c);
|
||||
SS_CHECK(all.back()->id == 3);
|
||||
|
||||
// Being on one list does not affect the other.
|
||||
ss::IntrusiveList<Conn>::remove(b);
|
||||
SS_CHECK(all.size() == 2 && idle.front() == &b);
|
||||
|
||||
int sum = 0;
|
||||
all.for_each([&](Conn &x) { sum += x.id; });
|
||||
SS_CHECK(sum == 4);
|
||||
SS_CHECK(all.pop_front()->id == 1);
|
||||
}
|
||||
|
||||
void test_list_unlinks_on_destruction()
|
||||
{
|
||||
ss::IntrusiveList<Conn> all;
|
||||
{
|
||||
Conn temp(9);
|
||||
all.push_back(temp);
|
||||
SS_CHECK(!all.empty());
|
||||
}
|
||||
SS_CHECK(all.empty());
|
||||
}
|
||||
|
||||
// A pooled, shared object: the pool owns the storage, references own the
|
||||
// object's lifetime.
|
||||
struct Session : ss::RefCounted<Session> {
|
||||
static inline ss::ObjectPool<Session> *pool = nullptr;
|
||||
static inline int destroyed = 0;
|
||||
|
||||
// The PoolPtr is parked inside the object while references exist.
|
||||
ss::PoolPtr<Session> self;
|
||||
|
||||
static void destroy(Session *s) noexcept
|
||||
{
|
||||
++destroyed;
|
||||
ss::PoolPtr<Session> owner = std::move(s->self);
|
||||
owner.reset(); // returns the slot to the pool
|
||||
}
|
||||
};
|
||||
|
||||
ss::RefPtr<Session> make_session()
|
||||
{
|
||||
auto slot = Session::pool->acquire();
|
||||
SS_CHECK(slot);
|
||||
Session *raw = slot->get();
|
||||
raw->self = std::move(slot).value();
|
||||
return ss::RefPtr<Session>(raw);
|
||||
}
|
||||
|
||||
void test_refptr_lifecycle()
|
||||
{
|
||||
ss::ObjectPool<Session> pool;
|
||||
SS_CHECK(pool.reserve(2));
|
||||
Session::pool = &pool;
|
||||
Session::destroyed = 0;
|
||||
{
|
||||
ss::RefPtr<Session> table = make_session();
|
||||
SS_CHECK(table->ref_count() == 1);
|
||||
ss::RefPtr<Session> relay = table;
|
||||
SS_CHECK(table->ref_count() == 2);
|
||||
ss::RefPtr<Session> moved = std::move(relay);
|
||||
SS_CHECK(!relay && moved->ref_count() == 2); // NOLINT(bugprone-use-after-move): checks moved-from state
|
||||
table.reset();
|
||||
SS_CHECK(Session::destroyed == 0 && pool.live() == 1);
|
||||
moved = nullptr;
|
||||
SS_CHECK(Session::destroyed == 1 && pool.live() == 0);
|
||||
}
|
||||
SS_CHECK(pool.available() == 2);
|
||||
}
|
||||
|
||||
void test_fixed_heap()
|
||||
{
|
||||
ss::FixedHeap heap;
|
||||
SS_CHECK(heap.reserve(4096));
|
||||
void *a = heap.allocate(10);
|
||||
void *b = heap.allocate_zeroed(4, 25);
|
||||
SS_CHECK(a && b && heap.owns(a) && heap.owns(b));
|
||||
SS_CHECK(reinterpret_cast<std::uintptr_t>(a) % 16 == 0);
|
||||
auto *zeros = static_cast<unsigned char *>(b);
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
SS_CHECK(zeros[i] == 0);
|
||||
}
|
||||
std::memcpy(a, "0123456789", 10);
|
||||
void *grown = heap.reallocate(a, 200);
|
||||
SS_CHECK(grown && std::memcmp(grown, "0123456789", 10) == 0);
|
||||
|
||||
auto before = heap.stats();
|
||||
heap.deallocate(grown);
|
||||
heap.deallocate(b);
|
||||
SS_CHECK(heap.stats().in_use < before.in_use);
|
||||
|
||||
// Freed blocks are reused instead of consuming fresh space.
|
||||
void *again = heap.allocate(200);
|
||||
SS_CHECK(again == grown);
|
||||
heap.deallocate(again);
|
||||
|
||||
SS_CHECK(heap.allocate(ss::FixedHeap::max_request() + 1) == nullptr);
|
||||
SS_CHECK(heap.allocate(8192) == nullptr); // over budget
|
||||
SS_CHECK(heap.stats().failures == 2);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_list_basic);
|
||||
SS_RUN(test_list_unlinks_on_destruction);
|
||||
SS_RUN(test_refptr_lifecycle);
|
||||
SS_RUN(test_fixed_heap);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
#include "core/pool.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
int g_destroyed = 0;
|
||||
|
||||
struct Plain {
|
||||
explicit Plain(int v) noexcept : value(v) {}
|
||||
~Plain() { ++g_destroyed; }
|
||||
int value;
|
||||
};
|
||||
|
||||
// Models an object that owns libuv handles: it can only be recycled once
|
||||
// its asynchronous closes have finished.
|
||||
struct Closable {
|
||||
static inline ss::CloseCompletion pending;
|
||||
static inline int begun = 0;
|
||||
|
||||
void begin_close(ss::CloseCompletion done) noexcept
|
||||
{
|
||||
++begun;
|
||||
pending = done;
|
||||
}
|
||||
~Closable() { ++g_destroyed; }
|
||||
};
|
||||
|
||||
struct ClosesSynchronously {
|
||||
void begin_close(ss::CloseCompletion done) noexcept { done(); }
|
||||
};
|
||||
|
||||
void test_acquire_release()
|
||||
{
|
||||
ss::ObjectPool<Plain> pool;
|
||||
SS_CHECK(pool.reserve(2));
|
||||
g_destroyed = 0;
|
||||
{
|
||||
auto a = pool.acquire(1);
|
||||
auto b = pool.acquire(2);
|
||||
SS_CHECK(a && b);
|
||||
SS_CHECK((*a)->value == 1 && (*b)->value == 2);
|
||||
SS_CHECK(pool.live() == 2 && pool.full());
|
||||
|
||||
auto c = pool.acquire(3);
|
||||
SS_CHECK(!c && c.error().code == ss::Errc::exhausted);
|
||||
}
|
||||
SS_CHECK(g_destroyed == 2);
|
||||
SS_CHECK(pool.live() == 0 && pool.available() == 2);
|
||||
}
|
||||
|
||||
void test_reuse_and_handles()
|
||||
{
|
||||
ss::ObjectPool<Plain> pool;
|
||||
SS_CHECK(pool.reserve(1));
|
||||
auto first = pool.acquire(10);
|
||||
SS_CHECK(first);
|
||||
ss::PoolPtr<Plain> owner = std::move(first).value();
|
||||
ss::Handle<Plain> h = pool.handle_of(owner.get());
|
||||
SS_CHECK(h);
|
||||
SS_CHECK(pool.resolve(h) == owner.get());
|
||||
|
||||
owner.reset();
|
||||
SS_CHECK(pool.resolve(h) == nullptr);
|
||||
|
||||
// The slot is reused, but the stale handle must not resolve to the new
|
||||
// occupant.
|
||||
auto second = pool.acquire(20);
|
||||
SS_CHECK(second);
|
||||
SS_CHECK(pool.resolve(h) == nullptr);
|
||||
SS_CHECK(pool.resolve(pool.handle_of(second->get())) == second->get());
|
||||
SS_CHECK(!pool.resolve(ss::Handle<Plain>{}));
|
||||
}
|
||||
|
||||
void test_async_close()
|
||||
{
|
||||
ss::ObjectPool<Closable> pool;
|
||||
SS_CHECK(pool.reserve(1));
|
||||
g_destroyed = 0;
|
||||
Closable::begun = 0;
|
||||
auto obj = pool.acquire();
|
||||
SS_CHECK(obj);
|
||||
ss::Handle<Closable> h = pool.handle_of(obj->get());
|
||||
|
||||
obj->reset();
|
||||
// Ownership is gone, but the memory is not reusable yet.
|
||||
SS_CHECK(Closable::begun == 1);
|
||||
SS_CHECK(g_destroyed == 0);
|
||||
SS_CHECK(pool.closing() == 1 && pool.available() == 0);
|
||||
SS_CHECK(pool.resolve(h) == nullptr);
|
||||
SS_CHECK(!pool.acquire());
|
||||
|
||||
Closable::pending();
|
||||
SS_CHECK(g_destroyed == 1);
|
||||
SS_CHECK(pool.closing() == 0 && pool.available() == 1);
|
||||
{
|
||||
auto again = pool.acquire();
|
||||
SS_CHECK(again);
|
||||
}
|
||||
// The pool refuses to die with a close still in flight, so finish it.
|
||||
SS_CHECK(pool.closing() == 1);
|
||||
Closable::pending();
|
||||
SS_CHECK(pool.closing() == 0);
|
||||
}
|
||||
|
||||
void test_sync_close()
|
||||
{
|
||||
ss::ObjectPool<ClosesSynchronously> pool;
|
||||
SS_CHECK(pool.reserve(1));
|
||||
{
|
||||
auto obj = pool.acquire();
|
||||
SS_CHECK(obj);
|
||||
}
|
||||
SS_CHECK(pool.available() == 1);
|
||||
}
|
||||
|
||||
void test_reserve_rules()
|
||||
{
|
||||
ss::ObjectPool<Plain> pool;
|
||||
SS_CHECK(!pool.acquire(1));
|
||||
SS_CHECK(!pool.reserve(0));
|
||||
SS_CHECK(pool.reserve(4));
|
||||
SS_CHECK(!pool.reserve(4));
|
||||
SS_CHECK(pool.capacity() == 4);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_acquire_release);
|
||||
SS_RUN(test_reuse_and_handles);
|
||||
SS_RUN(test_async_close);
|
||||
SS_RUN(test_sync_close);
|
||||
SS_RUN(test_reserve_rules);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "core/result.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
ss::Result<int> parse_positive(int v)
|
||||
{
|
||||
if (v <= 0) {
|
||||
return ss::fail(ss::Errc::invalid);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
ss::Status check_twice(int v)
|
||||
{
|
||||
SS_TRY(parse_positive(v));
|
||||
SS_TRY(parse_positive(v - 1));
|
||||
return {};
|
||||
}
|
||||
|
||||
void test_value_and_error()
|
||||
{
|
||||
auto ok = parse_positive(5);
|
||||
SS_CHECK(ok.has_value());
|
||||
SS_CHECK(*ok == 5);
|
||||
SS_CHECK(ok.value_or(0) == 5);
|
||||
|
||||
auto bad = parse_positive(-1);
|
||||
SS_CHECK(!bad);
|
||||
SS_CHECK(bad.error().code == ss::Errc::invalid);
|
||||
SS_CHECK(bad.value_or(42) == 42);
|
||||
}
|
||||
|
||||
void test_status_and_try()
|
||||
{
|
||||
SS_CHECK(check_twice(3).has_value());
|
||||
auto r = check_twice(1);
|
||||
SS_CHECK(!r);
|
||||
SS_CHECK(r.error() == ss::Error{ ss::Errc::invalid, 0 });
|
||||
}
|
||||
|
||||
void test_move_only_value()
|
||||
{
|
||||
auto make = []() -> ss::Result<std::unique_ptr<int> > { return std::make_unique<int>(7); };
|
||||
auto r = make();
|
||||
SS_CHECK(r);
|
||||
std::unique_ptr<int> p = std::move(r).value();
|
||||
SS_CHECK(*p == 7);
|
||||
}
|
||||
|
||||
void test_monadic()
|
||||
{
|
||||
auto doubled = parse_positive(4).transform([](int v) { return v * 2; });
|
||||
SS_CHECK(doubled && *doubled == 8);
|
||||
auto chained = parse_positive(4).and_then([](int v) { return parse_positive(v - 10); });
|
||||
SS_CHECK(!chained && chained.error().code == ss::Errc::invalid);
|
||||
}
|
||||
|
||||
void test_sys_errno()
|
||||
{
|
||||
ss::Result<int> r = ss::fail(ss::Errc::io, 104);
|
||||
SS_CHECK(r.error().sys == 104);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_value_and_error);
|
||||
SS_RUN(test_status_and_try);
|
||||
SS_RUN(test_move_only_value);
|
||||
SS_RUN(test_monadic);
|
||||
SS_RUN(test_sys_errno);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_TEST_HELPERS_HPP
|
||||
#define SS_TEST_HELPERS_HPP
|
||||
|
||||
// Minimal harness for the C++ unit tests. Tests are built without
|
||||
// exceptions, so a failed check reports its location and aborts.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#define SS_CHECK(...) \
|
||||
do { \
|
||||
if (!(__VA_ARGS__)) { \
|
||||
std::fprintf(stderr, "%s:%d: check failed: %s\n", __FILE__, \
|
||||
__LINE__, #__VA_ARGS__); \
|
||||
std::abort(); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define SS_RUN(test) \
|
||||
do { \
|
||||
test(); \
|
||||
std::printf("ok %s\n", #test); \
|
||||
} while (0)
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user