Q-W1 code-review follow-ups: restore 104 trailing newlines, relocate reasampler_uid.h to core/wire, drop ext_keys namespaces shim, add slot_map_tests, golden serialize literals for 4 modules, namespaces.h/pragma-once ordering sweep. 60/60 green.
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
// Standalone tests for reasampler::model::SlotMap — no REAPER, no test framework.
|
||||
// SlotMap is the L7 gap-preserving display-position carrier for one bank, extracted
|
||||
// from bank_book (Q-W1, T4-05). These are the pure SlotMap-only assertions that
|
||||
// previously lived inline in test_bank_book.cpp (the L7 "SlotMap unit behaviour"
|
||||
// block); test_bank_book.cpp keeps its BankBook-level integration coverage
|
||||
// (reorderSample / reconcileSlots / JSON round-trip WITH a full book), this file
|
||||
// owns the module's own contract: add/remove/query, reorder gap-preservation,
|
||||
// resetDense/reconcile, equality/fromEntries, and the serialize wire shape.
|
||||
|
||||
#include "../src/core/model/slot_map.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../src/core/json/json.h"
|
||||
|
||||
using namespace reasampler::model;
|
||||
namespace json = reasampler::json;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// --- SlotMap unit behaviour --------------------------------------------------
|
||||
|
||||
static void testSlotMapDenseAppend() {
|
||||
SlotMap m;
|
||||
m.append("a");
|
||||
m.append("b");
|
||||
m.append("c");
|
||||
CHECK(m.slotOf("a") == 0);
|
||||
CHECK(m.slotOf("b") == 1);
|
||||
CHECK(m.slotOf("c") == 2);
|
||||
CHECK(m.maxSlot() == 2);
|
||||
CHECK((m.orderedIds() == std::vector<std::string>{"a", "b", "c"}));
|
||||
CHECK(m.idAt(1) == "b");
|
||||
CHECK(m.slotOf("nope") == -1);
|
||||
}
|
||||
|
||||
static void testSlotMapRemoveLeavesGap() {
|
||||
SlotMap m;
|
||||
m.append("a"); m.append("b"); m.append("c"); // 0,1,2
|
||||
CHECK(m.remove("b")); // slot 1 now EMPTY (no re-pack)
|
||||
CHECK(m.slotOf("a") == 0);
|
||||
CHECK(m.slotOf("c") == 2); // c did NOT shift down
|
||||
CHECK(m.idAt(1).empty()); // gap preserved
|
||||
CHECK((m.orderedIds() == std::vector<std::string>{"a", "c"}));
|
||||
CHECK(!m.remove("b")); // already gone
|
||||
}
|
||||
|
||||
static void testSlotMapAppendAfterGapGoesToFrontier() {
|
||||
SlotMap m;
|
||||
m.append("a"); m.append("b"); m.append("c"); // 0,1,2
|
||||
m.remove("a"); // slot 0 empty
|
||||
m.append("d"); // append goes AFTER last occupied (2) -> 3
|
||||
CHECK(m.slotOf("d") == 3); // did NOT fill the slot-0 gap
|
||||
CHECK(m.idAt(0).empty());
|
||||
}
|
||||
|
||||
static void testSlotMapReorderIntoEmpty() {
|
||||
SlotMap m;
|
||||
m.append("a"); m.append("b"); m.append("c"); // 0,1,2
|
||||
m.remove("b"); // slot 1 empty
|
||||
CHECK(m.reorder("c", 1)); // c -> empty slot 1; its slot 2 empties
|
||||
CHECK(m.slotOf("c") == 1);
|
||||
CHECK(m.idAt(2).empty());
|
||||
CHECK(m.slotOf("a") == 0); // untouched
|
||||
}
|
||||
|
||||
static void testSlotMapReorderOntoOccupiedInsertsAndShifts() {
|
||||
SlotMap m;
|
||||
m.append("a"); m.append("b"); m.append("c"); m.append("d"); // 0,1,2,3
|
||||
CHECK(m.reorder("d", 1)); // d onto occupied slot 1 -> insert-before, shift b,c up
|
||||
CHECK(m.slotOf("a") == 0); // before the target: unchanged
|
||||
CHECK(m.slotOf("d") == 1); // took the target slot
|
||||
CHECK(m.slotOf("b") == 2); // shifted +1
|
||||
CHECK(m.slotOf("c") == 3); // shifted +1
|
||||
CHECK((m.orderedIds() == std::vector<std::string>{"a", "d", "b", "c"}));
|
||||
}
|
||||
|
||||
static void testSlotMapReorderPreservesInteriorGapAboveTarget() {
|
||||
SlotMap m;
|
||||
m.append("a"); m.append("b"); m.append("c"); // 0,1,2
|
||||
m.remove("b"); // gap at 1: a@0, c@2
|
||||
m.append("d"); // d@3
|
||||
CHECK(m.reorder("d", 0)); // d onto occupied slot 0 -> a shifts to 1, c shifts to 3
|
||||
CHECK(m.slotOf("d") == 0);
|
||||
CHECK(m.slotOf("a") == 1); // shifted from 0 -> 1
|
||||
CHECK(m.slotOf("c") == 3); // shifted from 2 -> 3 (gap at 2 preserved as a +1 of its own)
|
||||
CHECK(m.idAt(2).empty()); // interior gap above the target survives
|
||||
}
|
||||
|
||||
static void testSlotMapReorderUnmappedIsNoOp() {
|
||||
SlotMap m;
|
||||
m.append("a");
|
||||
CHECK(!m.reorder("ghost", 0)); // not mapped -> false, no mutation
|
||||
CHECK(m.slotOf("a") == 0);
|
||||
}
|
||||
|
||||
static void testSlotMapNegativeTargetClampsToZero() {
|
||||
SlotMap m;
|
||||
m.append("a"); m.append("b"); // 0,1
|
||||
CHECK(m.reorder("b", -3)); // clamp to 0 -> insert-before a
|
||||
CHECK(m.slotOf("b") == 0);
|
||||
CHECK(m.slotOf("a") == 1);
|
||||
}
|
||||
|
||||
static void testSlotMapResetDenseSkipsDupesAndEmpties() {
|
||||
SlotMap m;
|
||||
m.resetDense({"a", "", "b", "a", "c"}); // "" and the second "a" dropped
|
||||
CHECK((m.orderedIds() == std::vector<std::string>{"a", "b", "c"}));
|
||||
CHECK(m.slotOf("a") == 0);
|
||||
CHECK(m.slotOf("c") == 2);
|
||||
}
|
||||
|
||||
static void testSlotMapReconcileDropsStaleAppendsNew() {
|
||||
SlotMap m;
|
||||
m.append("a"); m.append("b"); m.append("c"); // 0,1,2
|
||||
m.reconcile({"a", "c", "d"}); // b left the index (drop), d is new (append)
|
||||
CHECK(m.slotOf("a") == 0); // kept at its slot
|
||||
CHECK(m.slotOf("c") == 2); // kept at its slot (gap where b was)
|
||||
CHECK(m.slotOf("b") == -1); // stale marker dropped
|
||||
CHECK(m.slotOf("d") == 3); // appended after the frontier
|
||||
CHECK(m.idAt(1).empty()); // b's slot stays empty
|
||||
}
|
||||
|
||||
static void testSlotMapEqualityAndFromEntries() {
|
||||
SlotMap a;
|
||||
a.append("x"); a.append("y");
|
||||
SlotMap b = SlotMap::fromEntries({{"x", 0}, {"y", 1}});
|
||||
CHECK(a == b);
|
||||
// Defensive repair: duplicate id (first wins), slot conflict (later dropped),
|
||||
// empty id / negative slot dropped.
|
||||
SlotMap c = SlotMap::fromEntries({{"x", 0}, {"x", 5}, {"y", 0}, {"", 9}, {"z", -1}, {"w", 2}});
|
||||
CHECK(c.slotOf("x") == 0); // first x wins
|
||||
CHECK(c.slotOf("y") == -1); // slot 0 already taken -> dropped
|
||||
CHECK(c.slotOf("w") == 2); // valid
|
||||
CHECK(c.slotOf("z") == -1); // negative slot dropped
|
||||
}
|
||||
|
||||
// --- serialize: golden byte-literal + round-trip -----------------------------
|
||||
|
||||
// Pins the exact wire shape (an array of {"id":..,"slot":..} objects, ascending
|
||||
// slot, no whitespace) so a future format drift is caught here rather than only
|
||||
// as a downstream bank_book diff. Mirrors the pre-extraction bank_book writer
|
||||
// byte-for-byte (core/json emit helpers are shared, not reimplemented).
|
||||
static void testSlotMapSerializeGoldenLiteral() {
|
||||
SlotMap empty;
|
||||
CHECK(empty.serialize() == "[]");
|
||||
|
||||
SlotMap m;
|
||||
m.append("a");
|
||||
m.append("b");
|
||||
CHECK(m.serialize() == "[{\"id\":\"a\",\"slot\":0},{\"id\":\"b\",\"slot\":1}]");
|
||||
}
|
||||
|
||||
// A local mirror of bank_book's private parseSlots (the "slots" array grammar):
|
||||
// [{id, slot}, ...]. slot_map.cpp itself only emits — JSON parsing is a consumer
|
||||
// concern (see slot_map.h) — so the round-trip proof below parses the emitted
|
||||
// text back into pairs the same way bank_book does, then rebuilds via
|
||||
// SlotMap::fromEntries and checks equality against the original.
|
||||
static bool parseSlotsArray(json::Reader& r, std::vector<std::pair<std::string, int>>& out) {
|
||||
out.clear();
|
||||
if (!r.consume('[')) return false;
|
||||
r.skipWs();
|
||||
if (r.consume(']')) return true; // empty array
|
||||
do {
|
||||
if (!r.consume('{')) return false;
|
||||
std::string id;
|
||||
int slot = 0;
|
||||
bool haveId = false, haveSlot = false;
|
||||
do {
|
||||
std::string k;
|
||||
if (!r.parseKey(k)) return false;
|
||||
if (k == "id") { if (!r.parseString(id)) return false; haveId = true; }
|
||||
else if (k == "slot") { if (!r.parseInt(slot)) return false; haveSlot = true; }
|
||||
else { if (!r.skipValue()) return false; }
|
||||
} while (r.consume(','));
|
||||
if (!r.consume('}')) return false;
|
||||
if (!haveId || !haveSlot) return false;
|
||||
out.emplace_back(std::move(id), slot);
|
||||
} while (r.consume(','));
|
||||
return r.consume(']');
|
||||
}
|
||||
|
||||
static void testSlotMapSerializeRoundTrip() {
|
||||
SlotMap m;
|
||||
m.append("a"); m.append("b"); m.append("c");
|
||||
m.remove("b"); // leave a gap: a@0, c@2
|
||||
m.append("d"); // d@3
|
||||
|
||||
const std::string blob = m.serialize();
|
||||
json::Reader r(blob);
|
||||
std::vector<std::pair<std::string, int>> pairs;
|
||||
CHECK(parseSlotsArray(r, pairs));
|
||||
|
||||
SlotMap round = SlotMap::fromEntries(pairs);
|
||||
CHECK(round == m);
|
||||
CHECK(round.slotOf("a") == 0);
|
||||
CHECK(round.idAt(1).empty()); // gap survives the round trip
|
||||
CHECK(round.slotOf("c") == 2);
|
||||
CHECK(round.slotOf("d") == 3);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testSlotMapDenseAppend();
|
||||
testSlotMapRemoveLeavesGap();
|
||||
testSlotMapAppendAfterGapGoesToFrontier();
|
||||
testSlotMapReorderIntoEmpty();
|
||||
testSlotMapReorderOntoOccupiedInsertsAndShifts();
|
||||
testSlotMapReorderPreservesInteriorGapAboveTarget();
|
||||
testSlotMapReorderUnmappedIsNoOp();
|
||||
testSlotMapNegativeTargetClampsToZero();
|
||||
testSlotMapResetDenseSkipsDupesAndEmpties();
|
||||
testSlotMapReconcileDropsStaleAppendsNew();
|
||||
testSlotMapEqualityAndFromEntries();
|
||||
testSlotMapSerializeGoldenLiteral();
|
||||
testSlotMapSerializeRoundTrip();
|
||||
|
||||
if (g_fail == 0) {
|
||||
std::printf("slot_map_tests: all passed\n");
|
||||
return 0;
|
||||
}
|
||||
std::printf("slot_map_tests: %d failure(s)\n", g_fail);
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user