#include "harness.hpp" #include #include #include #include #include #include #include PLOT_TEST(array_series_bounds_and_gather) { std::vector y{1, 5, -2, 3}; plot::data::ArraySeries s(std::move(y), /*x0=*/10.0, /*dx=*/0.5); PLOT_CHECK(s.size() == 4); PLOT_CHECK_NEAR(s.x_bounds().min, 10.0, 1e-9); PLOT_CHECK_NEAR(s.x_bounds().max, 11.5, 1e-9); PLOT_CHECK_NEAR(s.y_bounds().min, -2.0, 1e-6); std::vector out; s.gather(0, 0, 4, out); PLOT_CHECK(out.size() == 8); // interleaved x,y PLOT_CHECK_NEAR(out[0], 10.0, 1e-6); PLOT_CHECK_NEAR(out[1], 1.0, 1e-6); PLOT_CHECK_NEAR(out[6], 11.5, 1e-6); } PLOT_TEST(stream_series_acquire_publishes_snapshot) { plot::data::StreamSeries s(1 << 12, /*x0=*/0.0, /*dx=*/1e-6); for (int i = 0; i < 100; ++i) s.push(f32(i)); PLOT_CHECK(s.produced() == 100); s.acquire(); PLOT_CHECK(s.size() == 100); // Pyramid must track the published samples. PLOT_CHECK_NEAR(s.pyramid().reduce(0, 100).max, 99.0, 1e-4); for (int i = 100; i < 250; ++i) s.push(f32(i)); s.acquire(); PLOT_CHECK(s.size() == 250); } PLOT_TEST(stream_series_drops_oldest_when_full) { plot::data::StreamSeries s(256, 0.0, 1.0); for (int i = 0; i < 1000; ++i) s.push(f32(i)); s.acquire(); PLOT_CHECK(s.size() <= 256); PLOT_CHECK_NEAR(s.y_bounds().max, 999.0, 1e-4); } PLOT_TEST(array_series_exact_range_queries_match_bruteforce) { // The strong test for the exactness split: arbitrary, deliberately // unaligned ranges must match a brute-force scan of the raw samples. std::mt19937 rng(7); std::uniform_real_distribution d(-5.0f, 5.0f); std::vector y(4000); for (auto& v : y) v = d(rng); plot::data::ArraySeries s(y, 0.0, 1.0); for (u64 a = 0; a < 4000; a += 173) { for (u64 b = a + 1; b <= 4000; b += 331) { float lo = y[a], hi = y[a]; double sum = 0.0, sq = 0.0; for (u64 i = a; i < b; ++i) { lo = std::min(lo, y[i]); hi = std::max(hi, y[i]); sum += y[i]; sq += double(y[i]) * double(y[i]); } const plot::Range r = s.reduce(a, b); PLOT_CHECK_NEAR(r.min, lo, 1e-4); PLOT_CHECK_NEAR(r.max, hi, 1e-4); PLOT_CHECK_NEAR(s.sum(a, b), sum, 1e-2); PLOT_CHECK_NEAR(s.sumsq(a, b), sq, 1e-1); } } } PLOT_TEST(stream_series_exact_queries_survive_ring_wrap) { plot::data::StreamSeries s(1u << 10, 0.0, 1.0); constexpr u64 kPushes = 3000; constexpr u64 kFirstRetained = kPushes - (1u << 10); // 1976 for (u64 i = 0; i < kPushes; ++i) s.push(f32(i % 97)); // wraps the ring twice s.acquire(); const u64 n = s.size(); PLOT_CHECK(n == 1u << 10); PLOT_CHECK_NEAR(s.x0(), 1976.0, 1e-9); PLOT_CHECK(s.x_bounds().min == s.x0()); PLOT_CHECK(s.pyramid().size() == n); const plot::Range r = s.reduce(0, n); // retained window, wrapped PLOT_CHECK_NEAR(r.max, 96.0, 1e-4); PLOT_CHECK_NEAR(r.min, 0.0, 1e-4); double sum = 0.0, sumsq = 0.0; for (u64 i = kFirstRetained; i < kPushes; ++i) { const double v = static_cast(i % 97); sum += v; sumsq += v * v; } PLOT_CHECK_NEAR(s.sum(0, n), sum, 1e-2); PLOT_CHECK_NEAR(s.sumsq(0, n), sumsq, 1e-1); std::vector out; s.gather(0, 0, n, out); // level 0: raw samples PLOT_CHECK(out.size() == 2 * n); PLOT_CHECK_NEAR(out[0], s.x0(), 1e-6); PLOT_CHECK_NEAR(out[1], f32(kFirstRetained % 97), 1e-6); PLOT_CHECK_NEAR(out[2 * n - 2], s.x0() + f64(n - 1), 1e-6); } PLOT_TEST(stream_series_push_and_read_are_race_free) { // The ring overwrites, so the producer reuses the slot of the snapshot's oldest // retained sample while the consumer may still be reading it. Relaxed atomic // slots make that defined; nothing else in the suite has two threads, so this // is the case ThreadSanitizer actually flags when that is not the case. plot::data::StreamSeries s(1u << 8, 0.0, 1.0); constexpr u64 kPushes = 50'000; std::thread producer([&s] { for (u64 i = 0; i < kPushes; ++i) s.push(f32(i % 971)); }); u64 reads = 0; while (s.produced() < kPushes) { s.acquire(); const u64 n = s.size(); if (n == 0) continue; (void)s.reduce(0, n); (void)s.sum(0, n); (void)s.sumsq(0, n); std::vector out; s.gather(0, 0, n, out); // reads the oldest edge PLOT_CHECK(out.size() == 2 * n); ++reads; } producer.join(); PLOT_CHECK(s.produced() == kPushes); PLOT_CHECK(reads > 0); }