From c1029a25dfd18d79349581bbb6175f3e3fdc805e Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Thu, 27 Aug 2026 19:39:14 +0200 Subject: [PATCH] fix(udpscope): make the time-order test actually exercise the swap, guard the font copy The ramp data in EmitsPointsInTimeOrder never produced a bucket whose maximum preceded its minimum, so an implementation ordering the emitted pair by value instead of by time would have passed. Replaced with an explicit two-bucket case whose second bucket reverses the order. file(COPY) is a hard configure error on a missing source, so a checkout without the sibling StreamHub resources failed to configure despite the ASCII-icon fallback the block above had just selected. Plan amended to match on both points. Co-Authored-By: Claude Opus 4.6 --- Client/udpscope/CMakeLists.txt | 6 ++++- Client/udpscope/tests/DecimateTest.cpp | 17 ++++++++++++++ docs/superpowers/plans/2026-08-27-udpscope.md | 23 ++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/Client/udpscope/CMakeLists.txt b/Client/udpscope/CMakeLists.txt index 2d1a59f..e523cfa 100644 --- a/Client/udpscope/CMakeLists.txt +++ b/Client/udpscope/CMakeLists.txt @@ -72,7 +72,11 @@ else() message(WARNING "Bundled Font Awesome missing — using ASCII icon fallbacks") endif() -file(COPY ${FONT_DIR} DESTINATION ${CMAKE_BINARY_DIR}/resources) +# Guarded: file(COPY) is a hard configure error on a missing source, which +# would defeat the fallback the block above just chose. +if(EXISTS ${FONT_DIR}) + file(COPY ${FONT_DIR} DESTINATION ${CMAKE_BINARY_DIR}/resources) +endif() # ── Core library: everything except main.cpp, so tests can link it ──────────── set(CORE_SOURCES diff --git a/Client/udpscope/tests/DecimateTest.cpp b/Client/udpscope/tests/DecimateTest.cpp index 85a6d7e..2a87044 100644 --- a/Client/udpscope/tests/DecimateTest.cpp +++ b/Client/udpscope/tests/DecimateTest.cpp @@ -49,6 +49,23 @@ TEST(MinMaxDecimate, PreservesTheExtremesOfEveryBucket) { // A ring whose timestamps are not monotonic breaks any later binary search by // time, so the pair emitted per bucket must be ordered by time, not by value. TEST(MinMaxDecimate, EmitsPointsInTimeOrder) { + // Two buckets of four. In the first the minimum comes before the maximum, + // in the second the order is reversed. An implementation that emitted + // (min, max) by value rather than by time passes on bucket 0 and fails on + // bucket 1, so this data exercises the swap that a monotonically growing + // ramp never triggers. + const double st[8] = {0, 1, 2, 3, 4, 5, 6, 7}; + const double sv[8] = {-5, 0, 0, 9, 9, 0, 0, -5}; + Series pair; + MinMaxDecimate(st, sv, 8, 4, pair); + ASSERT_EQ(pair.size(), 4u); + const double wantT[4] = {0, 3, 4, 7}; + const double wantV[4] = {-5, 9, 9, -5}; + for (size_t i = 0; i < 4; i++) { + EXPECT_EQ(pair.t[i], wantT[i]) << "time at " << i; + EXPECT_EQ(pair.v[i], wantV[i]) << "value at " << i; + } + std::vector t(400), v(400); for (size_t i = 0; i < t.size(); i++) { t[i] = static_cast(i); diff --git a/docs/superpowers/plans/2026-08-27-udpscope.md b/docs/superpowers/plans/2026-08-27-udpscope.md index 75f8d2a..6350d39 100644 --- a/docs/superpowers/plans/2026-08-27-udpscope.md +++ b/docs/superpowers/plans/2026-08-27-udpscope.md @@ -128,6 +128,23 @@ TEST(MinMaxDecimate, PreservesTheExtremesOfEveryBucket) { // A ring whose timestamps are not monotonic breaks any later binary search by // time, so the pair emitted per bucket must be ordered by time, not by value. TEST(MinMaxDecimate, EmitsPointsInTimeOrder) { + // Two buckets of four. In the first the minimum comes before the maximum, + // in the second the order is reversed. An implementation that emitted + // (min, max) by value rather than by time passes on bucket 0 and fails on + // bucket 1, so this data exercises the swap that a monotonically growing + // ramp never triggers. + const double st[8] = {0, 1, 2, 3, 4, 5, 6, 7}; + const double sv[8] = {-5, 0, 0, 9, 9, 0, 0, -5}; + Series pair; + MinMaxDecimate(st, sv, 8, 4, pair); + ASSERT_EQ(pair.size(), 4u); + const double wantT[4] = {0, 3, 4, 7}; + const double wantV[4] = {-5, 9, 9, -5}; + for (size_t i = 0; i < 4; i++) { + EXPECT_EQ(pair.t[i], wantT[i]) << "time at " << i; + EXPECT_EQ(pair.v[i], wantV[i]) << "value at " << i; + } + std::vector t(400), v(400); for (size_t i = 0; i < t.size(); i++) { t[i] = static_cast(i); @@ -362,7 +379,11 @@ else() message(WARNING "Bundled Font Awesome missing — using ASCII icon fallbacks") endif() -file(COPY ${FONT_DIR} DESTINATION ${CMAKE_BINARY_DIR}/resources) +# Guarded: file(COPY) is a hard configure error on a missing source, which +# would defeat the fallback the block above just chose. +if(EXISTS ${FONT_DIR}) + file(COPY ${FONT_DIR} DESTINATION ${CMAKE_BINARY_DIR}/resources) +endif() # ── Core library: everything except main.cpp, so tests can link it ──────────── set(CORE_SOURCES