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:
Max Lv
2026-10-14 12:51:47 +08:00
parent 0799d3a9aa
commit f90cfe9034
20 changed files with 2789 additions and 3 deletions
+31
View File
@@ -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})
+63
View File
@@ -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;
}
+82
View File
@@ -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;
}
+148
View File
@@ -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;
}
+145
View File
@@ -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;
}
+140
View File
@@ -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;
}
+79
View File
@@ -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;
}
+26
View File
@@ -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