// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include #include #include #include "../image_puller/ReceiveRingBuffer.h" using namespace std::chrono_literals; TEST_CASE("ReceiveRingBuffer_sequential_reuse", "[ImagePuller][Unit]") { ReceiveRingBuffer ring(1024); // Far more messages than fit at once: each one is released before the next is taken. for (int i = 0; i < 100; i++) { auto r = ring.Allocate(300); REQUIRE(r); REQUIRE(r->size == 300); r->data[0] = 1; r->data[299] = 2; } CHECK(ring.GetUtilization() == 0); CHECK(ring.GetMaxUtilization() == 300); CHECK(ring.GetCapacity() == 1024); } TEST_CASE("ReceiveRingBuffer_wraps_to_the_start", "[ImagePuller][Unit]") { ReceiveRingBuffer ring(1000); auto a = ring.Allocate(400); auto b = ring.Allocate(400); REQUIRE(a); REQUIRE(b); uint8_t *a_data = a->data; // 200 bytes left at the end and nothing free at the start: a third 400 has to wait. auto f = std::async(std::launch::async, [&] { return ring.Allocate(400); }); CHECK(f.wait_for(50ms) == std::future_status::timeout); a.reset(); // frees [0, 400): the waiter wraps round to the start auto c = f.get(); REQUIRE(c); CHECK(c->data == a_data); CHECK(ring.GetUtilization() == 800); // Never two live regions on the same bytes CHECK((b->data + 400 <= c->data || c->data + 400 <= b->data)); } TEST_CASE("ReceiveRingBuffer_full_ring_blocks", "[ImagePuller][Unit]") { ReceiveRingBuffer ring(1000); auto a = ring.Allocate(500); auto b = ring.Allocate(500); // tail has come round to head REQUIRE(a); REQUIRE(b); uint8_t *a_data = a->data; auto f = std::async(std::launch::async, [&] { return ring.Allocate(1); }); CHECK(f.wait_for(50ms) == std::future_status::timeout); a.reset(); auto c = f.get(); REQUIRE(c); CHECK(c->data == a_data); } TEST_CASE("ReceiveRingBuffer_out_of_order_release_reclaims_in_order", "[ImagePuller][Unit]") { ReceiveRingBuffer ring(1000); auto a = ring.Allocate(500); auto b = ring.Allocate(500); REQUIRE(a); REQUIRE(b); b.reset(); // the newer region goes first - as republish may do CHECK(ring.GetUtilization() == 1000); // its bytes are behind a's and cannot be reused yet auto f = std::async(std::launch::async, [&] { return ring.Allocate(600); }); CHECK(f.wait_for(50ms) == std::future_status::timeout); a.reset(); // now both are reclaimed auto c = f.get(); REQUIRE(c); CHECK(ring.GetUtilization() == 600); } TEST_CASE("ReceiveRingBuffer_oversized_message_goes_to_heap", "[ImagePuller][Unit]") { ReceiveRingBuffer ring(100); auto small = ring.Allocate(100); REQUIRE(small); // Does not wait for the full ring - it never fits in it auto big = ring.Allocate(1000); REQUIRE(big); REQUIRE(big->size == 1000); big->data[999] = 1; CHECK(ring.GetUtilization() == 1100); CHECK(ring.GetMaxUtilization() == 1100); big.reset(); CHECK(ring.GetUtilization() == 100); } TEST_CASE("ReceiveRingBuffer_zero_size", "[ImagePuller][Unit]") { ReceiveRingBuffer ring(100); auto a = ring.Allocate(0); auto b = ring.Allocate(100); REQUIRE(a); REQUIRE(b); CHECK(a->size == 0); a.reset(); CHECK(ring.GetUtilization() == 100); b.reset(); CHECK(ring.GetUtilization() == 0); } TEST_CASE("ReceiveRingBuffer_stop_releases_waiter", "[ImagePuller][Unit]") { ReceiveRingBuffer ring(100); auto a = ring.Allocate(100); REQUIRE(a); auto f = std::async(std::launch::async, [&] { return ring.Allocate(50); }); CHECK(f.wait_for(50ms) == std::future_status::timeout); ring.Stop(); CHECK(f.get() == nullptr); } TEST_CASE("ReceiveRingBuffer_region_outlives_ring", "[ImagePuller][Unit]") { std::shared_ptr r; { ReceiveRingBuffer ring(100); r = ring.Allocate(50); } REQUIRE(r); r->data[49] = 1; r.reset(); }