xo-umbrella2/xo-callback2/utest/CallbackSet.test.cpp

223 lines
6.2 KiB
C++

/** @file CallbackSet.test.cpp
*
* @author Roland Conybeare, Aug 2026
**/
#include <xo/callback2/CallbackSet.hpp>
#include <xo/alloc2/arena/IAllocator_DArena.hpp>
#include <catch2/catch.hpp>
namespace ut {
using xo::fn::CallbackSet;
using xo::fn::CallbackId;
using xo::mm::AAllocator;
using xo::mm::DArena;
using xo::mm::ArenaConfig;
using xo::facet::with_facet;
using xo::facet::obj;
using xo::flatstring;
namespace {
using PlainSet = CallbackSet<void (*)(int)>;
/* callbacks are plain function pointers, so shared state has to be
* file-scope -- that is exactly the tradeoff the no-capture rule buys
*/
int s_sum = 0;
int s_count = 0;
void cb_add(int x) { s_sum += x; ++s_count; }
void cb_double(int x) { s_sum += 2*x; ++s_count; }
/* for the reentrancy cases */
PlainSet * s_set = nullptr;
CallbackId s_victim;
void cb_self_remove(int x) {
s_sum += x;
++s_count;
s_set->remove_callback(s_victim);
}
void cb_self_add(int x) {
s_sum += x;
++s_count;
s_set->add_callback(&cb_add);
}
void reset() { s_sum = 0; s_count = 0; s_set = nullptr; }
}
/* the traced/untraced split is a compile-time property */
static_assert(PlainSet::is_traced == false);
static_assert(CallbackSet<obj<AAllocator>>::is_traced == true);
TEST_CASE("cbset2-empty", "[callback2]") {
ArenaConfig cfg { .name_ = flatstring("cbset-arena"), .size_ = 4*1024 };
DArena arena = DArena::map(cfg);
auto mm = with_facet<AAllocator>::mkobj(&arena);
PlainSet * s = PlainSet::_empty(mm, 8);
REQUIRE(s != nullptr);
CHECK(s->capacity() == 8);
CHECK(s->size() == 0);
CHECK(s->is_empty());
CHECK(!s->is_full());
CHECK(!s->is_running());
}
TEST_CASE("cbset2-add-invoke", "[callback2]") {
reset();
ArenaConfig cfg { .name_ = flatstring("cbset-arena"), .size_ = 4*1024 };
DArena arena = DArena::map(cfg);
auto mm = with_facet<AAllocator>::mkobj(&arena);
PlainSet * s = PlainSet::_empty(mm, 8);
REQUIRE(s);
CallbackId id1 = s->add_callback(&cb_add);
CallbackId id2 = s->add_callback(&cb_double);
CHECK(id1 != id2);
CHECK(s->size() == 2);
s->invoke(5);
/* 5 + 10 */
CHECK(s_sum == 15);
CHECK(s_count == 2);
CHECK(!s->is_running());
}
TEST_CASE("cbset2-remove", "[callback2]") {
reset();
ArenaConfig cfg { .name_ = flatstring("cbset-arena"), .size_ = 4*1024 };
DArena arena = DArena::map(cfg);
auto mm = with_facet<AAllocator>::mkobj(&arena);
PlainSet * s = PlainSet::_empty(mm, 8);
REQUIRE(s);
CallbackId id1 = s->add_callback(&cb_add);
s->add_callback(&cb_double);
CHECK(s->remove_callback(id1) == true);
CHECK(s->size() == 1);
/* removing twice is a no-op, not an error */
CHECK(s->remove_callback(id1) == false);
s->invoke(5);
CHECK(s_sum == 10); /* only cb_double survived */
CHECK(s_count == 1);
}
TEST_CASE("cbset2-full", "[callback2]") {
ArenaConfig cfg { .name_ = flatstring("cbset-arena"), .size_ = 4*1024 };
DArena arena = DArena::map(cfg);
auto mm = with_facet<AAllocator>::mkobj(&arena);
PlainSet * s = PlainSet::_empty(mm, 2);
REQUIRE(s);
CHECK(s->add_callback(&cb_add) != CallbackId());
CHECK(s->add_callback(&cb_add) != CallbackId());
CHECK(s->is_full());
/* capacity is fixed: the third add fails rather than reallocating */
CHECK(s->add_callback(&cb_add) == CallbackId());
CHECK(s->size() == 2);
}
TEST_CASE("cbset2-copy-grow", "[callback2]") {
reset();
ArenaConfig cfg { .name_ = flatstring("cbset-arena"), .size_ = 8*1024 };
DArena arena = DArena::map(cfg);
auto mm = with_facet<AAllocator>::mkobj(&arena);
PlainSet * s = PlainSet::_empty(mm, 2);
REQUIRE(s);
s->add_callback(&cb_add);
s->add_callback(&cb_double);
PlainSet * bigger = PlainSet::copy(mm, s, 8);
REQUIRE(bigger != nullptr);
CHECK(bigger->capacity() == 8);
CHECK(bigger->size() == 2);
CHECK(!bigger->is_full());
/* callbacks survived the copy */
bigger->invoke(5);
CHECK(s_sum == 15);
/* shrinking below size is refused */
CHECK(PlainSet::copy(mm, s, 1) == nullptr);
}
TEST_CASE("cbset2-reentrant-remove", "[callback2]") {
reset();
ArenaConfig cfg { .name_ = flatstring("cbset-arena"), .size_ = 4*1024 };
DArena arena = DArena::map(cfg);
auto mm = with_facet<AAllocator>::mkobj(&arena);
PlainSet * s = PlainSet::_empty(mm, 8);
REQUIRE(s);
s_set = s;
s->add_callback(&cb_self_remove);
s_victim = s->add_callback(&cb_double);
/* cb_self_remove removes cb_double WHILE the set is running.
* The removal must be deferred, so cb_double still runs this time.
*/
s->invoke(5);
CHECK(s_count == 2);
CHECK(s_sum == 15);
CHECK(s->size() == 1); /* deferred removal applied at end */
CHECK(!s->is_running());
/* second run: only cb_self_remove remains */
reset();
s_set = s;
s->invoke(5);
CHECK(s_count == 1);
CHECK(s_sum == 5);
}
TEST_CASE("cbset2-reentrant-add", "[callback2]") {
reset();
ArenaConfig cfg { .name_ = flatstring("cbset-arena"), .size_ = 4*1024 };
DArena arena = DArena::map(cfg);
auto mm = with_facet<AAllocator>::mkobj(&arena);
PlainSet * s = PlainSet::_empty(mm, 8);
REQUIRE(s);
s_set = s;
s->add_callback(&cb_self_add);
/* the added callback must NOT run during the invoke that added it */
s->invoke(5);
CHECK(s_count == 1);
CHECK(s->size() == 2); /* deferred add applied at end */
/* now both run */
reset();
s_set = s;
s->invoke(5);
CHECK(s_count == 2);
}
} /*namespace ut*/
/* end CallbackSet.test.cpp */