From 4e0e0ae0b3c7994484737c251b391cf16a1ca25e Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 28 Sep 2026 13:53:26 +0200 Subject: [PATCH] test(plot): region removal re-indexes the ids and the statistics cache --- tests/test_plot.cpp | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/test_plot.cpp b/tests/test_plot.cpp index 01bd236..0b54602 100644 --- a/tests/test_plot.cpp +++ b/tests/test_plot.cpp @@ -1590,3 +1590,54 @@ PLOT_TEST(region_stats_track_a_sliding_streaming_window) { PLOT_CHECK_NEAR(step.mean, 0.5 * (step.min + 60.0), 1e-9); } } + +/// Region ids are vector indices, so removing one shifts every id above it -- and the +/// statistics cache is keyed by that index, so it has to be re-indexed with the vector +/// rather than left naming the regions it was filled for. A stale key here would answer +/// for a *different* interval than the one asked about, which a count alone cannot see: +/// the three intervals below are all 11 samples wide, so these assertions are about the +/// extremes each id actually names. +PLOT_TEST(removing_a_region_shifts_the_ids_and_keeps_the_statistics) { + plot::data::ArraySeries s(ramp(100), 0.0, 1.0); + plot::Plot p; + p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f}); + const u32 ax = p.add_x_axis({"t", {0.0, 99.0}, false, plot::AxisPlace::Bottom, false}); + const u32 ay = p.add_y_axis({"v", {0.0, 99.0}, false, plot::AxisPlace::Left, false}); + const u32 tr = p.add_trace({&s, plot::Color::hex(0xFFFFFF), 1.0f, ax, ay}); + + plot::Region bounds; + bounds.snap = plot::RegionSnap::Free; // the intervals under test are these numbers + bounds.x0 = 10.0; + bounds.x1 = 20.0; + const u32 a = p.add_region(bounds); + bounds.x0 = 30.0; + bounds.x1 = 40.0; + const u32 b = p.add_region(bounds); + bounds.x0 = 50.0; + bounds.x1 = 60.0; + const u32 c = p.add_region(bounds); + PLOT_CHECK(a == 0u && b == 1u && c == 2u); + PLOT_CHECK(p.region_count() == 3u); + // Filled on demand, in the order the caller asks: three entries, one per pair. + PLOT_CHECK_NEAR(p.region_stats(a, tr).min, 10.0, 1e-9); + PLOT_CHECK_NEAR(p.region_stats(b, tr).min, 30.0, 1e-9); + PLOT_CHECK_NEAR(p.region_stats(c, tr).min, 50.0, 1e-9); + + p.remove_region(a); + PLOT_CHECK(p.region_count() == 2u); + PLOT_CHECK_NEAR(p.region(0).x0, 30.0, 1e-12); // the old region 1 + PLOT_CHECK_NEAR(p.region(1).x0, 50.0, 1e-12); // the old region 2 + // The surviving entries follow their regions down, so each id still answers for its own + // interval rather than for the one that used to hold that index. + PLOT_CHECK_NEAR(p.region_stats(0u, tr).min, 30.0, 1e-9); + PLOT_CHECK_NEAR(p.region_stats(0u, tr).max, 40.0, 1e-9); + PLOT_CHECK_NEAR(p.region_stats(1u, tr).min, 50.0, 1e-9); + PLOT_CHECK_NEAR(p.region_stats(1u, tr).max, 60.0, 1e-9); + // The id the removed region held now names the region that shifted into it -- there is + // no hole in the vector -- and an id past the end names nothing and reads a default + // rather than out of range. Neither has any statistics. + PLOT_CHECK_NEAR(p.region(0u).x0, 30.0, 1e-12); + PLOT_CHECK_NEAR(p.region(2u).x0, 0.0, 1e-12); + PLOT_CHECK(!p.region_stats(2u, tr).valid); + PLOT_CHECK(!p.region_stats(9u, tr).valid); +}