// Standalone tests for reasampler::bank_grid — no REAPER, no test framework. // Same fast loop as the sibling pure tests: assert the grid-layout math and the // thumbnail cache-key stringification directly. // // Covers (M5 Wave A brief §Test cases): column count for a given panel width; // cell rects for a full grid (row/column wrapping); the partial-last-row case; // itemCount == 0; a single item; a panel too narrow for even one cell (clamp to // one column); content-height for exact and partial rows; cache-key stability, // width/generation/id sensitivity, and length-prefix collision resistance. // // Wave B adds: hit-testing (inside / gap / out-of-range / half-open bounds); // selection updates (plain / ctrl-toggle / shift-range, invariants preserved); and // keyboard nav (arrow clamp, row moves, shift-extend, partial-last-row clamp, // fresh-panel focus). #include "../src/core/ui/bank_grid.h" #include #include #include #include using namespace reasampler; using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // A spec with round numbers so expected rects are trivial to hand-compute: // cell 100x50, gap 10. Column pitch = 110, row pitch = 60, margin = 10. static GridSpec spec() { GridSpec s; s.cellWidth = 100; s.cellHeight = 50; s.gap = 10; return s; } // gap(10) + n*(100+10): 1 col needs 120, 2 need 230, 3 need 340. static void testColumnsForWidth() { const GridSpec s = spec(); CHECK(columnsForWidth(120, s) == 1); // exactly one cell + margins CHECK(columnsForWidth(229, s) == 1); // one pixel short of two columns CHECK(columnsForWidth(230, s) == 2); // exactly two CHECK(columnsForWidth(345, s) == 3); // three plus slack CHECK(columnsForWidth(1000, s) == 9); // many } // A panel narrower than a single cell still yields one column (clipped by the // window, never zero — that would drop every sample). static void testTooNarrowClampsToOneColumn() { const GridSpec s = spec(); CHECK(columnsForWidth(50, s) == 1); CHECK(columnsForWidth(0, s) == 1); CHECK(columnsForWidth(-20, s) == 1); } // Zero items -> no rects (empty state is the panel's concern, not the layout's). static void testZeroItems() { const GridSpec s = spec(); auto rects = computeCellRects(0, 500, s); CHECK(rects.empty()); CHECK(contentHeight(0, 500, s) == 0); } // One item sits at the top-left margin. static void testSingleItem() { const GridSpec s = spec(); auto rects = computeCellRects(1, 500, s); CHECK(rects.size() == 1); CHECK(rects[0] == (CellRect{10, 10, 100, 50})); } // A full 2x2: width forces exactly two columns, four items fill two rows. static void testFullGridWrapping() { const GridSpec s = spec(); // Width 230 -> exactly 2 columns. auto rects = computeCellRects(4, 230, s); CHECK(rects.size() == 4); // Row 0: x = 10, 120 ; y = 10. CHECK(rects[0] == (CellRect{10, 10, 100, 50})); CHECK(rects[1] == (CellRect{120, 10, 100, 50})); // Row 1: y = 70 (10 + 60). CHECK(rects[2] == (CellRect{10, 70, 100, 50})); CHECK(rects[3] == (CellRect{120, 70, 100, 50})); } // Partial last row: 5 items in a 2-column grid -> rows of 2,2,1. The lone last // cell is left-aligned in its row (no centering), same x as column 0. static void testPartialLastRow() { const GridSpec s = spec(); auto rects = computeCellRects(5, 230, s); // 2 columns CHECK(rects.size() == 5); CHECK(rects[4] == (CellRect{10, 130, 100, 50})); // row 2, col 0: y = 10 + 2*60 // content height spans 3 rows: 10 + 3*(50+10) = 190. CHECK(contentHeight(5, 230, s) == 190); } // Content height for an exact-fill grid: 4 items / 2 cols = 2 rows. static void testContentHeightExactRows() { const GridSpec s = spec(); CHECK(contentHeight(4, 230, s) == 130); // 10 + 2*60 CHECK(contentHeight(2, 230, s) == 70); // 10 + 1*60 } // The cache key is stable and sensitive to every field. static void testCacheKeyStabilityAndSensitivity() { ThumbnailKey a{"sample-1", 120, 7}; ThumbnailKey aSame{"sample-1", 120, 7}; CHECK(thumbnailKeyString(a) == thumbnailKeyString(aSame)); // deterministic ThumbnailKey diffWidth{"sample-1", 121, 7}; ThumbnailKey diffGen{"sample-1", 120, 8}; ThumbnailKey diffId{"sample-2", 120, 7}; CHECK(thumbnailKeyString(a) != thumbnailKeyString(diffWidth)); CHECK(thumbnailKeyString(a) != thumbnailKeyString(diffGen)); CHECK(thumbnailKeyString(a) != thumbnailKeyString(diffId)); } // A sampleId containing the delimiter byte must not forge a collision with a // different key. Without the length prefix, id "x|9" with width 0 and id "x" with // width 9 would both tail-concatenate through the '|' delimiter and could match; // the length prefix on the id disambiguates them. static void testCacheKeyDelimiterCollisionResistance() { // The canonical would-be collision: moving a "|9" fragment from the id into // the width field. Length-prefixing the id makes the two forms distinct. ThumbnailKey a{"x|9", 0, 0}; ThumbnailKey b{"x", 9, 0}; CHECK(thumbnailKeyString(a) != thumbnailKeyString(b)); // A second pair in the same spirit, delimiters in both id and adjacent fields. ThumbnailKey k1{"1|2", 3, 0}; ThumbnailKey k2{"1", 23, 0}; CHECK(thumbnailKeyString(k1) != thumbnailKeyString(k2)); } // --- Hit-testing -------------------------------------------------------------- // A 2x2 grid of the round spec: rects at (10,10),(120,10),(10,70),(120,70), each // 100x50. Gaps sit at x in [110,120) and y in [60,70) and the outer margin < 10. static std::vector grid2x2() { return computeCellRects(4, 230, spec()); // 2 columns, 4 items } // A point inside a cell returns that cell's index; the top-left corner is inside // (half-open lower bound), the bottom-right corner is NOT (half-open upper bound). static void testHitTestInside() { auto rects = grid2x2(); CHECK(hitTestCell(10, 10, rects) == 0); // top-left corner of cell 0: inside CHECK(hitTestCell(60, 35, rects) == 0); // center of cell 0 CHECK(hitTestCell(120, 10, rects) == 1); // top-left of cell 1 CHECK(hitTestCell(10, 70, rects) == 2); // top-left of cell 2 CHECK(hitTestCell(120, 70, rects) == 3); // top-left of cell 3 // One pixel inside the far edge of cell 0 (x=109,y=59) still hits it. CHECK(hitTestCell(109, 59, rects) == 0); } // The exclusive far edge (x+width, y+height) is a MISS — belongs to no cell, so // adjacent gapless rects would never double-claim it. static void testHitTestHalfOpenBounds() { auto rects = grid2x2(); CHECK(hitTestCell(110, 35, rects) == -1); // x == cell0.x+width: past cell 0 CHECK(hitTestCell(60, 60, rects) == -1); // y == cell0.y+height: past cell 0 } // A click in the inter-cell gap or the outer margin hits nothing. static void testHitTestGapAndMargin() { auto rects = grid2x2(); CHECK(hitTestCell(115, 35, rects) == -1); // horizontal gap between cols CHECK(hitTestCell(60, 65, rects) == -1); // vertical gap between rows CHECK(hitTestCell(0, 0, rects) == -1); // top-left margin CHECK(hitTestCell(5, 35, rects) == -1); // left margin } // A click well outside the grid (below the last row / right of the last col) and // an empty rect list both miss. static void testHitTestOutOfRange() { auto rects = grid2x2(); CHECK(hitTestCell(1000, 1000, rects) == -1); CHECK(hitTestCell(60, 35, {}) == -1); // no cells at all CHECK(hitTestCell(-5, -5, rects) == -1); // negative coords } // --- Selection updates -------------------------------------------------------- static bool selEq(const Selection& s, std::vector idx, int focus, int anchor) { return s.indices == idx && s.focus == focus && s.anchor == anchor; } // A plain click selects only that cell; focus and anchor both land on it, // replacing any prior multi-selection. static void testClickPlainReplaces() { Selection start{{0, 1, 2}, 2, 0}; Selection s = applyClick(start, 4, /*ctrl=*/false, /*shift=*/false, 6); CHECK(selEq(s, {4}, 4, 4)); } // Ctrl-click adds an unselected cell (keeping the set sorted) and moves focus. static void testClickCtrlAdds() { Selection start{{1, 3}, 3, 1}; Selection s = applyClick(start, 2, /*ctrl=*/true, /*shift=*/false, 6); CHECK(selEq(s, {1, 2, 3}, 2, 2)); // inserted in sorted position } // Ctrl-click on an already-selected cell removes it (toggle out); focus still // moves to the clicked cell even though it left the set. static void testClickCtrlRemoves() { Selection start{{1, 2, 3}, 3, 1}; Selection s = applyClick(start, 2, /*ctrl=*/true, /*shift=*/false, 6); CHECK(selEq(s, {1, 3}, 2, 2)); } // Shift-click selects the inclusive range from the existing anchor to the click, // leaving the anchor put; order-agnostic (anchor above or below the click). static void testClickShiftRange() { Selection start{{2}, 2, 2}; // anchor at 2 Selection s = applyClick(start, 5, /*ctrl=*/false, /*shift=*/true, 8); CHECK(selEq(s, {2, 3, 4, 5}, 5, 2)); // Downward range (click above the anchor) yields the same inclusive set. Selection s2 = applyClick(start, 0, false, true, 8); CHECK(selEq(s2, {0, 1, 2}, 0, 2)); } // Shift-click with no prior anchor behaves like a plain click (anchor seeds at the // clicked cell). static void testClickShiftNoAnchor() { Selection start{}; // focus/anchor == -1 Selection s = applyClick(start, 3, false, true, 6); CHECK(selEq(s, {3}, 3, 3)); } // Shift takes precedence over ctrl when both are held (range select, documented). static void testClickShiftBeatsCtrl() { Selection start{{1}, 1, 1}; Selection s = applyClick(start, 3, /*ctrl=*/true, /*shift=*/true, 6); CHECK(selEq(s, {1, 2, 3}, 3, 1)); // range, not toggle } // An out-of-range index (or empty grid) returns the selection unchanged. static void testClickOutOfRangeNoop() { Selection start{{1, 2}, 2, 1}; CHECK(applyClick(start, 9, false, false, 6) == start); CHECK(applyClick(start, -1, false, false, 6) == start); CHECK(applyClick(start, 0, false, false, 0) == start); } // --- Keyboard navigation ------------------------------------------------------ // Right/Left move by one in linear order; Down/Up move by a row (cols cells). static void testNavArrowsMoveOneAndRow() { // 6 items, 3 columns: rows [0,1,2],[3,4,5]. Focus at 1. Selection start{{1}, 1, 1}; CHECK(selEq(navigate(start, NavKey::Right, 3, 6, false), {2}, 2, 2)); CHECK(selEq(navigate(start, NavKey::Left, 3, 6, false), {0}, 0, 0)); CHECK(selEq(navigate(start, NavKey::Down, 3, 6, false), {4}, 4, 4)); // Up from row 1 back to row 0. Selection row1{{4}, 4, 4}; CHECK(selEq(navigate(row1, NavKey::Up, 3, 6, false), {1}, 1, 1)); } // Movement clamps at every edge (no wrap): Left on cell 0, Right on the last cell, // Up on the top row, Down past the last cell all stay put. static void testNavClampsAtEdges() { CHECK(selEq(navigate(Selection{{0}, 0, 0}, NavKey::Left, 3, 6, false), {0}, 0, 0)); CHECK(selEq(navigate(Selection{{5}, 5, 5}, NavKey::Right, 3, 6, false), {5}, 5, 5)); CHECK(selEq(navigate(Selection{{2}, 2, 2}, NavKey::Up, 3, 6, false), {2}, 2, 2)); CHECK(selEq(navigate(Selection{{5}, 5, 5}, NavKey::Down, 3, 6, false), {5}, 5, 5)); } // Down from a cell above a MISSING last-row cell clamps to the last cell rather // than overshooting past itemCount. 5 items, 3 cols: rows [0,1,2],[3,4]. Down from // 2 would be 5 (absent) -> clamps to 4. static void testNavDownPartialLastRowClamps() { Selection start{{2}, 2, 2}; CHECK(selEq(navigate(start, NavKey::Down, 3, 5, false), {4}, 4, 4)); } // Home/End jump to the first/last cell. static void testNavHomeEnd() { Selection start{{3}, 3, 3}; CHECK(selEq(navigate(start, NavKey::Home, 3, 6, false), {0}, 0, 0)); CHECK(selEq(navigate(start, NavKey::End, 3, 6, false), {5}, 5, 5)); } // Shift+arrow extends the range from the anchor; the anchor stays put as focus // walks. Repeated shift-right grows the set. static void testNavShiftExtends() { Selection start{{1}, 1, 1}; // anchor at 1 Selection s1 = navigate(start, NavKey::Right, 3, 6, true); CHECK(selEq(s1, {1, 2}, 2, 1)); Selection s2 = navigate(s1, NavKey::Right, 3, 6, true); CHECK(selEq(s2, {1, 2, 3}, 3, 1)); // Shift-down from the grown range extends by a whole row from the anchor. Selection s3 = navigate(s1, NavKey::Down, 3, 6, true); // focus 2 -> 5 CHECK(selEq(s3, {1, 2, 3, 4, 5}, 5, 1)); } // Shift-extending back toward the anchor shrinks the range (focus crosses the // anchor without moving it). static void testNavShiftShrinksAndCrosses() { Selection start{{1, 2, 3}, 3, 1}; // anchor 1, focus 3 Selection s = navigate(start, NavKey::Left, 3, 6, true); // focus 3 -> 2 CHECK(selEq(s, {1, 2}, 2, 1)); Selection s2 = navigate(s, NavKey::Left, 3, 6, true); // focus 2 -> 1 (anchor) CHECK(selEq(s2, {1}, 1, 1)); Selection s3 = navigate(s2, NavKey::Left, 3, 6, true); // cross below anchor CHECK(selEq(s3, {0, 1}, 0, 1)); } // A fresh panel (empty selection) focuses cell 0 on the first arrow without // stepping, with and without shift. static void testNavFromEmptyFocusesFirst() { Selection empty{}; CHECK(selEq(navigate(empty, NavKey::Down, 3, 6, false), {0}, 0, 0)); CHECK(selEq(navigate(empty, NavKey::Right, 3, 6, true), {0}, 0, 0)); } // itemCount <= 0 returns the selection unchanged; cols < 1 is treated as 1. static void testNavDegenerate() { Selection start{{1}, 1, 1}; CHECK(navigate(start, NavKey::Right, 3, 0, false) == start); // cols coerced to 1: Down moves by 1 in a single-column grid. CHECK(selEq(navigate(Selection{{0}, 0, 0}, NavKey::Down, 0, 4, false), {1}, 1, 1)); } // --- compressAmplitudeForDisplay ---------------------------------------------- // Full scale: magnitude 1.0 must reach the full display fraction exactly. static void testCompressFullScale() { CHECK(compressAmplitudeForDisplay(1.0f) == 1.0f); CHECK(compressAmplitudeForDisplay(-1.0f) == -1.0f); } // Exact zero must stay on the midline (no log of zero; guards the singularity). static void testCompressZeroIsMidline() { CHECK(compressAmplitudeForDisplay(0.0f) == 0.0f); } // -20 dB (0.1 linear) and -40 dB (0.01 linear) must both produce clearly visible // (non-zero) fractions, with -20 dB > -40 dB (monotonic), and both well above // the midline (arbitrary threshold of 0.15 chosen conservatively — at a -60 dB // floor, -20 dB normalizes to 2/3 and -40 dB to 1/3). static void testCompressMidValuesVisible() { const float f20 = compressAmplitudeForDisplay(0.1f); // -20 dBFS const float f40 = compressAmplitudeForDisplay(0.01f); // -40 dBFS CHECK(f20 > 0.15f); // clearly non-zero CHECK(f40 > 0.15f); // clearly non-zero CHECK(f20 > f40); // monotonic: louder -> taller bar } // At and below the floor (-60 dB = 0.001 linear) the result is ~0 (silence). // We test at exactly the floor magnitude and well below it. static void testCompressAtAndBelowFloor() { // 0.001 == 10^(-60/20) is the floor ratio. Magnitude at or below it -> 0. const float floorMag = std::pow(10.0f, kDisplayFloorDb / 20.0f); // ~0.001 CHECK(compressAmplitudeForDisplay(floorMag) == 0.0f); CHECK(compressAmplitudeForDisplay(floorMag * 0.5f) == 0.0f); CHECK(compressAmplitudeForDisplay(0.0001f) == 0.0f); } // Sign is preserved: negative input produces a negative fraction of the same // magnitude as its positive counterpart. static void testCompressSignPreserved() { const float pos = compressAmplitudeForDisplay(0.1f); const float neg = compressAmplitudeForDisplay(-0.1f); CHECK(neg < 0.0f); // Magnitudes must be equal (sign-symmetric). const float diff = pos + neg; // pos - |neg| CHECK(diff > -0.001f && diff < 0.001f); } int main() { testColumnsForWidth(); testTooNarrowClampsToOneColumn(); testZeroItems(); testSingleItem(); testFullGridWrapping(); testPartialLastRow(); testContentHeightExactRows(); testCacheKeyStabilityAndSensitivity(); testCacheKeyDelimiterCollisionResistance(); testHitTestInside(); testHitTestHalfOpenBounds(); testHitTestGapAndMargin(); testHitTestOutOfRange(); testClickPlainReplaces(); testClickCtrlAdds(); testClickCtrlRemoves(); testClickShiftRange(); testClickShiftNoAnchor(); testClickShiftBeatsCtrl(); testClickOutOfRangeNoop(); testNavArrowsMoveOneAndRow(); testNavClampsAtEdges(); testNavDownPartialLastRowClamps(); testNavHomeEnd(); testNavShiftExtends(); testNavShiftShrinksAndCrosses(); testNavFromEmptyFocusesFirst(); testNavDegenerate(); testCompressFullScale(); testCompressZeroIsMidline(); testCompressMidValuesVisible(); testCompressAtAndBelowFloor(); testCompressSignPreserved(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }