Files
plotter/tests/test_ring_buffer.cpp
Martino FerrariandClaude Opus 4.6 3ad02e5120
CI / debug (tests + all adapters) (push) Canceled after 0s
CI / coverage (floor + measured table) (push) Canceled after 0s
CI / asan (AddressSanitizer) (push) Canceled after 0s
feat: a cleared ring can resume its absolute count at a given origin
A derived ring that is emptied and refilled from its source's retained
window has to come back numbered as the source is. `clear()` restarts it
at 0, which leaves every absolute index -- `base()`, `produced()`,
`copy_from()` -- short by that origin for a whole window's worth of data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-15 16:30:05 +02:00

99 lines
3.9 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "harness.hpp"
#include <plot/data/ring_buffer.hpp>
PLOT_TEST(ring_wraps_and_keeps_latest) {
plot::data::RingBuffer<f32> r(8); // capacity rounded up to a power of two
for (int i = 0; i < 20; ++i) r.push(f32(i));
PLOT_CHECK(r.size() == 8);
PLOT_CHECK_NEAR(r[0], 12.0); // oldest retained
PLOT_CHECK_NEAR(r[7], 19.0); // newest
}
PLOT_TEST(ring_clear_resets) {
plot::data::RingBuffer<f32> r(8);
for (int i = 0; i < 5; ++i) r.push(f32(i));
r.clear();
PLOT_CHECK(r.size() == 0);
r.push(42.0f);
PLOT_CHECK(r.size() == 1);
PLOT_CHECK_NEAR(r[0], 42.0);
}
PLOT_TEST(ring_clear_to_resumes_the_absolute_count) {
// A recompute: a derived ring is emptied and refilled from the absolute origin its source
// retains, so it has to come back numbered as the source is. `clear()` restarts at 0, which
// leaves a refilled ring reporting a produced() a whole window behind an identically fed one.
plot::data::RingBuffer<f32> src(8);
plot::data::RingBuffer<f32> derived(8);
for (int i = 0; i < 20; ++i) {
src.push(f32(i));
derived.push(f32(i) * 2.0f);
}
PLOT_CHECK(src.base() == 12);
derived.clear_to(src.base());
for (u64 i = 0; i < src.size(); ++i) derived.push(src[i] * 3.0f);
// Numbered as the source: same produced(), same base(), and the retained windows pair up.
PLOT_CHECK(derived.produced() == src.produced());
PLOT_CHECK(derived.base() == src.base());
PLOT_CHECK(derived.size() == src.size());
PLOT_CHECK_NEAR(derived[0], 36.0); // 12 * 3
PLOT_CHECK_NEAR(derived[7], 57.0); // 19 * 3
// And the absolute reader agrees with the windowed one, which is what a snapshot reads.
f32 at = 0.0f;
derived.copy_from(19, 1, &at);
PLOT_CHECK_NEAR(at, 57.0);
}
PLOT_TEST(ring_clear_to_zero_is_clear) {
plot::data::RingBuffer<f32> r(8);
for (int i = 0; i < 5; ++i) r.push(f32(i));
r.clear_to(0);
PLOT_CHECK(r.size() == 0);
PLOT_CHECK(r.produced() == 0);
r.push(42.0f);
PLOT_CHECK_NEAR(r[0], 42.0);
}
PLOT_TEST(ring_capacity_rounds_up_to_a_power_of_two) {
// The capacity is rounded up so slot addressing stays a mask, and capacity()
// reports the rounded value -- the one callers must size for (R4).
PLOT_CHECK(plot::data::RingBuffer<f32>(100).capacity() == 128);
PLOT_CHECK(plot::data::RingBuffer<f32>(1).capacity() == 1);
PLOT_CHECK(plot::data::RingBuffer<f32>(0).capacity() == 1);
PLOT_CHECK(plot::data::RingBuffer<f32>(64).capacity() == 64); // already a power
plot::data::RingBuffer<f32> r(100);
for (int i = 0; i < 1000; ++i) r.push(f32(i));
PLOT_CHECK(r.size() == 128); // size() saturates at the rounded capacity
PLOT_CHECK_NEAR(r[0], 872.0); // oldest retained
PLOT_CHECK_NEAR(r[127], 999.0); // newest
}
PLOT_TEST(ring_amend_rewrites_a_retained_slot) {
// Backfill: a slot pushed as a placeholder is rewritten once the data that belongs there
// arrives. The index is absolute, because that is the only coordinate a late packet can
// compute -- a retained-window index would have moved by the time it got here.
plot::data::RingBuffer<f32> r(8);
for (int i = 0; i < 6; ++i) r.push(f32(i));
PLOT_CHECK(r.amend(3, 42.0f));
PLOT_CHECK_NEAR(r[3], 42.0);
PLOT_CHECK(r.produced() == 6); // an amend is not a push
}
PLOT_TEST(ring_amend_refuses_an_index_the_ring_no_longer_holds) {
// The window slides on the producer, so a late amend may arrive after its slot was
// overwritten. Writing it anyway would corrupt whatever sample now owns that slot, so the
// bounds test is the whole safety of the operation and the caller reads the result.
plot::data::RingBuffer<f32> r(8);
for (int i = 0; i < 20; ++i) r.push(f32(i));
PLOT_CHECK(r.base() == 12);
PLOT_CHECK(!r.amend(3, 42.0f)); // aged out
PLOT_CHECK(!r.amend(20, 42.0f)); // never produced
PLOT_CHECK_NEAR(r[0], 12.0); // and nothing moved
PLOT_CHECK_NEAR(r[7], 19.0);
}