package dev.dtrentin.chart.buffer import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class TieredBufferTest { private fun outTs() = LongArray(TieredBuffer.TOTAL_CAPACITY) private fun outVs() = FloatArray(TieredBuffer.TOTAL_CAPACITY) /** * Regression test for T8: legitimate sample equal to former sentinel value * (Float.MAX_VALUE) must not be dropped or misclassified by tier1 bin * accumulator. After T8 swaps sentinel-based "empty" detection for explicit * hasData flag, MAX_VALUE samples are retained. */ @Test fun tier1BinAcceptsFloatMaxValueSample() { val buf = TieredBuffer() // All 4 pushes in tier0 (5min retention); tier1 bin = 100ms. // ts=0,50,99 share bin [0,100); ts=100 rolls bin and flushes prior. buf.push(0L, Float.MAX_VALUE) buf.push(50L, Float.MAX_VALUE) buf.push(99L, Float.MAX_VALUE) buf.push(100L, 1f) val outTs = LongArray(TieredBuffer.TOTAL_CAPACITY) val outV = FloatArray(TieredBuffer.TOTAL_CAPACITY) // Wide window covering tier0 region — should include raw samples. val n = buf.snapshot(0L, 10_000L, outTs, outV) assertTrue(n >= 4, "expected at least 4 samples in tier0 snapshot, got $n") var found = 0 for (i in 0 until n) { if (outV[i] == Float.MAX_VALUE) found++ } assertTrue(found >= 3, "expected ≥3 MAX_VALUE samples retained, got $found") } /** * T12: tier1 M4 bin emits first/min/max/last with their original timestamps, * preserving visual shape. Snapshot via direct tier1 access (no tier0 fallback) * by forcing time advancement well past tier0 retention. */ @Test fun tier1BinPreservesM4() { val buf = TieredBuffer() // Tier1 bin = 100ms. Push 4 distinct samples into bin [0, 100): // ts=0 v=5 (first) // ts=30 v=1 (min) // ts=60 v=10 (max) // ts=90 v=3 (last) buf.push(0L, 5f) buf.push(30L, 1f) buf.push(60L, 10f) buf.push(90L, 3f) // Roll bin: push a sample past 100ms to trigger flush of prior bin. buf.push(100L, 7f) // Force snapshot to read from tier1 (not tier0) by advancing time past // tier0 retention. Push sample at tier0-window-far-future. val farFuture = TieredBuffer.TIER0_DURATION_MS + 200L buf.push(farFuture, 0f) val outTs = LongArray(TieredBuffer.TOTAL_CAPACITY) val outV = FloatArray(TieredBuffer.TOTAL_CAPACITY) // Window covers ts=[0, 100) — should hit tier1 (not tier0, since now - TIER0_DUR > 100). val n = buf.snapshot(0L, 100L, outTs, outV) assertTrue(n in 1..4, "expected 1..4 M4 records, got $n") // Strict chronological order. for (i in 1 until n) { assertTrue(outTs[i] > outTs[i - 1], "ts must be strictly increasing across M4 records at i=$i: ${outTs[i - 1]} -> ${outTs[i]}") } // First record = (ts=0, v=5f) assertEquals(0L, outTs[0]) assertEquals(5f, outV[0]) // Last record = (ts=90, v=3f) assertEquals(90L, outTs[n - 1]) assertEquals(3f, outV[n - 1]) // Min (1f) and max (10f) must appear in the snapshot. var hasMin = false; var hasMax = false for (i in 0 until n) { if (outV[i] == 1f) hasMin = true if (outV[i] == 10f) hasMax = true } assertTrue(hasMin, "min value 1f missing from M4 records") assertTrue(hasMax, "max value 10f missing from M4 records") } /** * T12: tier2 M4 bin (1Hz, 1000ms) analogous to tier1 test. * Forces snapshot to read from tier2 by pushing past tier0+tier1 retention. */ @Test fun tier2BinPreservesM4() { val buf = TieredBuffer() // Tier2 bin = 1000ms. Push 4 distinct samples into bin [0, 1000): // ts=0 v=2 (first) // ts=300 v=8 (max) // ts=600 v=1 (min) // ts=900 v=5 (last) buf.push(0L, 2f) buf.push(300L, 8f) buf.push(600L, 1f) buf.push(900L, 5f) // Roll bin: push sample past 1000ms to flush prior tier2 bin. buf.push(1000L, 4f) // Force snapshot to read from tier2 by advancing past tier0 + tier1 retention. val farFuture = TieredBuffer.TIER0_DURATION_MS + TieredBuffer.TIER1_DURATION_MS + 2000L buf.push(farFuture, 0f) val outTs = LongArray(TieredBuffer.TOTAL_CAPACITY) val outV = FloatArray(TieredBuffer.TOTAL_CAPACITY) // Window covers ts=[0, 1000) — should hit tier2 only. val n = buf.snapshot(0L, 1000L, outTs, outV) assertTrue(n in 1..4, "expected 1..4 M4 records in tier2, got $n") // Strict chronological order. for (i in 1 until n) { assertTrue(outTs[i] > outTs[i - 1], "tier2 ts must be strictly increasing at i=$i") } // First & last anchors. assertEquals(0L, outTs[0]) assertEquals(2f, outV[0]) assertEquals(900L, outTs[n - 1]) assertEquals(5f, outV[n - 1]) var hasMin = false; var hasMax = false for (i in 0 until n) { if (outV[i] == 1f) hasMin = true if (outV[i] == 8f) hasMax = true } assertTrue(hasMin, "tier2 min 1f missing") assertTrue(hasMax, "tier2 max 8f missing") } /** * T12 (C7 fix): adjacent bins must not collide at the same timestamp. * Previous (midTs, min)/(midTs, max) pair produced a vertical spike at flush time; * with M4 each record carries its original ts → strict monotonicity preserved * across bin boundaries. */ @Test fun noVerticalSpikesAtBinBoundaries() { val buf = TieredBuffer() // Bin A [0, 100): push samples that span the bin's value range. buf.push(10L, 4f) buf.push(40L, 1f) buf.push(70L, 9f) buf.push(99L, 3f) // Bin B [100, 200): distinct values. buf.push(110L, 6f) buf.push(140L, 2f) buf.push(170L, 8f) buf.push(199L, 5f) // Flush bin B by triggering a rollover. buf.push(200L, 7f) // Force snapshot from tier1. val farFuture = TieredBuffer.TIER0_DURATION_MS + 500L buf.push(farFuture, 0f) val outTs = LongArray(TieredBuffer.TOTAL_CAPACITY) val outV = FloatArray(TieredBuffer.TOTAL_CAPACITY) val n = buf.snapshot(0L, 200L, outTs, outV) assertTrue(n >= 4, "expected ≥4 tier1 records across two bins, got $n") // Strict monotonicity across all records, including the bin A→B boundary. for (i in 1 until n) { assertTrue( outTs[i] > outTs[i - 1], "C7 regression: ts not strictly increasing at i=$i (${outTs[i - 1]} -> ${outTs[i]})", ) } } // ---------- T15: empty state ---------- @Test fun snapshot_emptyBuffer_returnsZero() { val buf = TieredBuffer() val n = buf.snapshot(0L, 10_000L, outTs(), outVs()) assertEquals(0, n) } @Test fun latestTimestampMs_emptyBuffer_returnsNegative() { val buf = TieredBuffer() assertEquals(-1L, buf.latestTimestampMs()) } // ---------- T15: tier0 only ---------- @Test fun tier0_singleSample_snapshotReturnsIt() { val buf = TieredBuffer() buf.push(100L, 1.5f) val ts = outTs(); val vs = outVs() val n = buf.snapshot(0L, 10_000L, ts, vs) assertEquals(1, n) assertEquals(100L, ts[0]) assertEquals(1.5f, vs[0]) } @Test fun tier0_multipleSamples_preservesOrder() { val buf = TieredBuffer() val srcTs = longArrayOf(10L, 20L, 30L, 40L, 50L) val srcVs = floatArrayOf(1f, 2f, 3f, 4f, 5f) for (i in srcTs.indices) buf.push(srcTs[i], srcVs[i]) val ts = outTs(); val vs = outVs() val n = buf.snapshot(0L, 10_000L, ts, vs) assertEquals(5, n) for (i in 1 until n) { assertTrue(ts[i] >= ts[i - 1], "expected non-decreasing ts at i=$i") } // Values match source in order. for (i in 0 until n) { assertEquals(srcTs[i], ts[i]) assertEquals(srcVs[i], vs[i]) } } // ---------- T15: tier roll boundaries ---------- @Test fun tier1Roll_singleBinFlush() { val buf = TieredBuffer() // 3 samples in tier1 bin [0, 100), then roll bin with ts=100. buf.push(10L, 1f) buf.push(40L, 2f) buf.push(80L, 3f) buf.push(100L, 4f) // Force tier1 snapshot path: push far-future. val farFuture = TieredBuffer.TIER0_DURATION_MS + 300L buf.push(farFuture, 9f) val ts = outTs(); val vs = outVs() val n = buf.snapshot(0L, 100L, ts, vs) assertTrue(n >= 1, "expected ≥1 tier1 flushed record, got $n") // All records inside [0, 100). for (i in 0 until n) { assertTrue(ts[i] in 0L..99L, "tier1 record out of window at i=$i: ${ts[i]}") } } @Test fun tier1Roll_emptyBinSkipped() { val buf = TieredBuffer() // Single sample in bin [0, 100). buf.push(50L, 7f) // Jump several empty bins ahead → roll directly to bin [500, 600). buf.push(550L, 8f) // Push another to flush bin [500, 600). buf.push(600L, 9f) // Force tier1 path. val farFuture = TieredBuffer.TIER0_DURATION_MS + 300L buf.push(farFuture, 0f) val ts = outTs(); val vs = outVs() // Window over the empty span [100, 500): expect zero records (no spurious flush). val n = buf.snapshot(100L, 400L, ts, vs) assertEquals(0, n, "empty bins between sparse samples must not produce records") } @Test fun tier2Roll_singleBinFlush() { val buf = TieredBuffer() // tier2 bin = 1000ms. buf.push(100L, 1f) buf.push(400L, 2f) buf.push(800L, 3f) // Roll tier2 bin. buf.push(1000L, 4f) // Force tier2 snapshot: push past tier0 + tier1 retention. val farFuture = TieredBuffer.TIER0_DURATION_MS + TieredBuffer.TIER1_DURATION_MS + 2000L buf.push(farFuture, 9f) val ts = outTs(); val vs = outVs() val n = buf.snapshot(0L, 1000L, ts, vs) assertTrue(n >= 1, "expected ≥1 tier2 record, got $n") for (i in 0 until n) { assertTrue(ts[i] in 0L..999L, "tier2 record out of window at i=$i: ${ts[i]}") } } // ---------- T15: window crossing tier boundaries ---------- @Test fun snapshot_windowSpansT0T1Boundary() { val buf = TieredBuffer() // Old data destined for tier1 flush. buf.push(0L, 1f) buf.push(50L, 2f) buf.push(99L, 3f) // Flush bin [0,100). buf.push(100L, 4f) // Advance now past tier0 retention so old data exits tier0. val nowTs = TieredBuffer.TIER0_DURATION_MS + 1000L buf.push(nowTs, 5f) // Recent sample fresh in tier0. buf.push(nowTs + 100L, 6f) val ts = outTs(); val vs = outVs() // Window spans from before tier0Boundary to now → should pick up both tier1 and tier0 ranges. val n = buf.snapshot(0L, nowTs + 200L, ts, vs) assertTrue(n >= 2, "expected ≥2 samples across t0/t1, got $n") // Chronological non-decreasing. for (i in 1 until n) { assertTrue(ts[i] >= ts[i - 1], "non-monotonic at i=$i: ${ts[i - 1]} -> ${ts[i]}") } } @Test fun snapshot_windowSpansT1T2Boundary() { val buf = TieredBuffer() // Very old samples for tier2. buf.push(0L, 1f) buf.push(500L, 2f) // Roll tier2 bin. buf.push(1000L, 3f) // Mid-age samples for tier1. val tier1Start = TieredBuffer.TIER1_DURATION_MS buf.push(tier1Start, 10f) buf.push(tier1Start + 50L, 11f) buf.push(tier1Start + 100L, 12f) // Force snapshot past both tier0 + tier1 retention. val nowTs = TieredBuffer.TIER0_DURATION_MS + TieredBuffer.TIER1_DURATION_MS + 2000L buf.push(nowTs, 99f) val ts = outTs(); val vs = outVs() // Wide window covering both tier1 and tier2 regions. val n = buf.snapshot(0L, nowTs + 100L, ts, vs) assertTrue(n >= 2, "expected ≥2 records across t1/t2, got $n") for (i in 1 until n) { assertTrue(ts[i] >= ts[i - 1], "non-monotonic at i=$i") } } @Test fun snapshot_windowSpansAll3Tiers() { val buf = TieredBuffer() // Oldest tier2 sample. buf.push(0L, 1f) buf.push(1000L, 2f) // flush bin [0,1000) // Mid tier1 region. val tier1Start = TieredBuffer.TIER1_DURATION_MS buf.push(tier1Start, 5f) buf.push(tier1Start + 100L, 6f) // flush bin // Recent tier0. val nowTs = TieredBuffer.TIER0_DURATION_MS + TieredBuffer.TIER1_DURATION_MS + 1000L buf.push(nowTs, 10f) buf.push(nowTs + 50L, 11f) val ts = outTs(); val vs = outVs() val n = buf.snapshot(0L, nowTs + 200L, ts, vs) assertTrue(n >= 3, "expected ≥3 records across all tiers, got $n") for (i in 1 until n) { assertTrue(ts[i] >= ts[i - 1], "non-monotonic at i=$i: ${ts[i - 1]} -> ${ts[i]}") } } // ---------- T15: clear() ---------- @Test fun clear_resetsBuffer() { val buf = TieredBuffer() for (i in 0 until 10) buf.push(i * 10L, i.toFloat()) buf.clear() val n = buf.snapshot(0L, 10_000L, outTs(), outVs()) assertEquals(0, n) assertEquals(-1L, buf.latestTimestampMs()) } @Test fun clear_resetsBinAccumulators() { val buf = TieredBuffer() // Seed tier1 bin [0, 100) with 3 samples — pre-flush, accumulator holds state. buf.push(10L, 100f) buf.push(40L, 200f) buf.push(80L, 300f) buf.clear() // Push 1 fresh sample into bin [0, 100) and roll. buf.push(20L, 1f) buf.push(100L, 2f) // roll flushes the post-clear single-sample bin // Force tier1 snapshot. val farFuture = TieredBuffer.TIER0_DURATION_MS + 300L buf.push(farFuture, 0f) val ts = outTs(); val vs = outVs() val n = buf.snapshot(0L, 100L, ts, vs) assertTrue(n >= 1, "expected ≥1 post-clear tier1 record, got $n") // Records must only reflect post-clear data (single sample at ts=20, v=1f). for (i in 0 until n) { assertEquals(20L, ts[i], "stale ts leaked through clear at i=$i") assertEquals(1f, vs[i], "stale value leaked through clear at i=$i") } } // ---------- T15: edge cases ---------- @Test fun snapshot_windowEntirelyBeforeData_returnsZero() { val buf = TieredBuffer() buf.push(10_000L, 1f) buf.push(10_500L, 2f) val n = buf.snapshot(0L, 5_000L, outTs(), outVs()) assertEquals(0, n) } @Test fun snapshot_windowEntirelyAfterData_returnsZero() { val buf = TieredBuffer() buf.push(500L, 1f) buf.push(1_000L, 2f) val n = buf.snapshot(10_000L, 10_000L, outTs(), outVs()) assertEquals(0, n) } @Test fun tier1BinHandlesSamePostFlushTimestamp() { val buf = TieredBuffer() // Bin [0, 100): single sample. buf.push(50L, 1f) // Roll to bin [100, 200) at exactly ts=100. buf.push(100L, 2f) // Another sample at the same ts=100 within new bin. buf.push(100L, 3f) // Continue with monotonic-non-decreasing ts. buf.push(150L, 4f) buf.push(200L, 5f) // roll again val ts = outTs(); val vs = outVs() // Wide window via tier0 fallback — no crash expected, sane ordering. val n = buf.snapshot(0L, 1_000L, ts, vs) assertTrue(n >= 5, "expected raw samples retained in tier0, got $n") for (i in 1 until n) { assertTrue(ts[i] >= ts[i - 1], "monotonic-non-decreasing ts violated at i=$i") } } }