Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green

This commit is contained in:
2026-07-28 20:48:56 -04:00
parent 67a41728f3
commit 847936f813
222 changed files with 2247 additions and 2079 deletions
+31 -30
View File
@@ -6,12 +6,13 @@
// present AND absent), dedup-by-hash collapse, tier filter + tier move,
// relative-path invariant, empty-index round-trip, malformed/truncated JSON.
#include "../src/bank_model.h"
#include "../src/core/model/bank_model.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::model;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -65,12 +66,12 @@ static Sample minimalSample(const std::string& seed) {
}
static void testFullFieldRoundTrip() {
BankIndex idx;
BankModel idx;
CHECK(idx.add(fullSample("a")) == AddResult::Added);
CHECK(idx.add(minimalSample("b")) == AddResult::Added);
std::string json = idx.serialize();
auto back = BankIndex::deserialize(json);
auto back = BankModel::deserialize(json);
CHECK(back.has_value());
CHECK(back && *back == idx);
@@ -104,7 +105,7 @@ static void testFullFieldRoundTrip() {
}
static void testDedupByHash() {
BankIndex idx;
BankModel idx;
Sample a = fullSample("x");
CHECK(idx.add(a) == AddResult::Added);
@@ -128,7 +129,7 @@ static void testDedupByHash() {
}
static void testTierFilterAndMove() {
BankIndex idx;
BankModel idx;
Sample scratch = minimalSample("s"); scratch.tier = Tier::Scratch;
Sample archive = fullSample("a"); archive.tier = Tier::Archive;
CHECK(idx.add(scratch) == AddResult::Added);
@@ -153,7 +154,7 @@ static void testTierFilterAndMove() {
}
static void testRelativePathInvariant() {
BankIndex idx;
BankModel idx;
// POSIX absolute, Windows drive, Windows backslash, UNC — all rejected.
const char* absolutes[] = {
@@ -184,7 +185,7 @@ static void testRelativePathInvariant() {
// bypasses dedup (an in-place refresh is not a new insert). The relative-paths-only
// invariant still guards the replacement.
static void testUpdateInPlace() {
BankIndex idx;
BankModel idx;
CHECK(idx.add(minimalSample("a")) == AddResult::Added); // id "min-a"
CHECK(idx.add(minimalSample("b")) == AddResult::Added); // id "min-b"
CHECK(idx.add(minimalSample("c")) == AddResult::Added); // id "min-c"
@@ -221,10 +222,10 @@ static void testUpdateInPlace() {
}
static void testEmptyIndexRoundTrip() {
BankIndex idx;
BankModel idx;
CHECK(idx.empty());
std::string json = idx.serialize();
auto back = BankIndex::deserialize(json);
auto back = BankModel::deserialize(json);
CHECK(back.has_value());
CHECK(back && back->empty());
CHECK(back && *back == idx);
@@ -244,17 +245,17 @@ static void testMalformedJson() {
"{\"samples\":[{\"createdTimestamp\":notanumber}]}",
};
for (const char* j : bad) {
auto r = BankIndex::deserialize(j);
auto r = BankModel::deserialize(j);
CHECK(!r.has_value()); // signaled as nullopt, no crash / UB
}
// A well-formed empty object deserializes to an empty index (lenient root).
auto ok = BankIndex::deserialize("{}");
auto ok = BankModel::deserialize("{}");
CHECK(ok.has_value() && ok->empty());
}
static void testRemoveAndQuery() {
BankIndex idx;
BankModel idx;
CHECK(idx.add(fullSample("1")) == AddResult::Added);
CHECK(idx.add(fullSample("2")) == AddResult::Added);
CHECK(idx.query("id-1") != nullptr);
@@ -267,7 +268,7 @@ static void testRemoveAndQuery() {
// Fix 1: drive-relative and bare-drive forms must be rejected by add().
static void testAbsolutePathDriveRelative() {
BankIndex idx;
BankModel idx;
// Drive-relative: resolves against the drive's CWD, not the project root.
Sample dr = minimalSample("dr");
@@ -295,7 +296,7 @@ static void testAbsolutePathDriveRelative() {
static void testUnicodeEscapeDecoding() {
// é = U+00E9 → 2-byte UTF-8: 0xC3 0xA9
// JSON: "é"
auto r1 = BankIndex::deserialize(
auto r1 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"u1\",\"relativePath\":\"bank/u.wav\","
"\"displayName\":\"\\u00e9\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -319,7 +320,7 @@ static void testUnicodeEscapeDecoding() {
// 中 = U+4E2D → 3-byte UTF-8: 0xE4 0xB8 0xAD
// JSON: "中"
auto r2 = BankIndex::deserialize(
auto r2 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"u2\",\"relativePath\":\"bank/u.wav\","
"\"displayName\":\"\\u4e2d\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -343,7 +344,7 @@ static void testUnicodeEscapeDecoding() {
}
// 😀 = U+1F600 → surrogate pair 😀 → 4-byte UTF-8: 0xF0 0x9F 0x98 0x80
auto r3 = BankIndex::deserialize(
auto r3 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"u3\",\"relativePath\":\"bank/u.wav\","
"\"displayName\":\"\\uD83D\\uDE00\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -368,7 +369,7 @@ static void testUnicodeEscapeDecoding() {
}
// Unpaired high surrogate (no following \uDCxx) → nullopt.
auto r4 = BankIndex::deserialize(
auto r4 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"u4\",\"relativePath\":\"bank/u.wav\","
"\"displayName\":\"\\uD83D\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -384,7 +385,7 @@ static void testUnicodeEscapeDecoding() {
// Fix 3: strtoll overflow must reject the value, not clamp it silently.
static void testIntegerOverflow() {
// A timestamp value that overflows int64_t (> 9223372036854775807).
auto r = BankIndex::deserialize(
auto r = BankModel::deserialize(
"{\"samples\":[{\"id\":\"ov1\",\"relativePath\":\"bank/ov.wav\","
"\"displayName\":\"\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -400,7 +401,7 @@ static void testIntegerOverflow() {
// Fix 4: out-of-range enum values must reject the sample, not produce invalid enum.
static void testEnumRangeValidation() {
// tier: 99 is not a valid Tier enumerator.
auto r1 = BankIndex::deserialize(
auto r1 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"en1\",\"relativePath\":\"bank/en.wav\","
"\"displayName\":\"\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -413,7 +414,7 @@ static void testEnumRangeValidation() {
CHECK(!r1.has_value());
// sourceMode: 99 is not a valid SourceMode enumerator.
auto r2 = BankIndex::deserialize(
auto r2 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"en2\",\"relativePath\":\"bank/en.wav\","
"\"displayName\":\"\","
"\"sourceMode\":99,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -442,7 +443,7 @@ static void testLegacyJsonDefaults() {
"\"key\":null,\"levels\":{\"peakDb\":0.0,\"rmsDb\":0.0,\"lufs\":0.0},"
"\"clipped\":false,\"tier\":0,\"contentHash\":\"h-leg1\","
"\"provenance\":null,\"createdTimestamp\":0}]}";
auto r = BankIndex::deserialize(legacy);
auto r = BankModel::deserialize(legacy);
CHECK(r.has_value());
if (r) {
const Sample* s = r->query("leg1");
@@ -453,7 +454,7 @@ static void testLegacyJsonDefaults() {
// Re-serialize is lossless: parsing it again yields an equal index. This
// proves the absent fields did not silently gain values on the way out.
std::string out = r->serialize();
auto again = BankIndex::deserialize(out);
auto again = BankModel::deserialize(out);
CHECK(again.has_value());
CHECK(again && *again == *r);
if (again) {
@@ -471,7 +472,7 @@ static void testLegacyJsonDefaults() {
// storing a bogus value.
static void testSeamFieldBoundaries() {
// rootNote at both MIDI edges + equal-and-end-anchored loop points round-trip.
BankIndex idx;
BankModel idx;
Sample lo = minimalSample("lo"); lo.contentHash = "h-lo";
lo.rootNote = 0;
lo.loop = LoopPoints{0, 0}; // zero-length marker at frame 0
@@ -484,7 +485,7 @@ static void testSeamFieldBoundaries() {
CHECK(idx.add(hi) == AddResult::Added);
CHECK(idx.add(end) == AddResult::Added);
auto back = BankIndex::deserialize(idx.serialize());
auto back = BankModel::deserialize(idx.serialize());
CHECK(back.has_value());
CHECK(back && *back == idx);
if (back) {
@@ -514,21 +515,21 @@ static void testSeamFieldBoundaries() {
"\"clipped\":false,\"tier\":0,\"contentHash\":\"h-bad\","
"\"provenance\":null,\"createdTimestamp\":0}]}";
CHECK(!BankIndex::deserialize(std::string(head) + "\"rootNote\":128," + tail).has_value());
CHECK(!BankIndex::deserialize(std::string(head) + "\"rootNote\":-1," + tail).has_value());
CHECK(!BankIndex::deserialize(
CHECK(!BankModel::deserialize(std::string(head) + "\"rootNote\":128," + tail).has_value());
CHECK(!BankModel::deserialize(std::string(head) + "\"rootNote\":-1," + tail).has_value());
CHECK(!BankModel::deserialize(
std::string(head) + "\"loop\":{\"start\":10,\"end\":5}," + tail).has_value()); // start > end
CHECK(!BankIndex::deserialize(
CHECK(!BankModel::deserialize(
std::string(head) + "\"loop\":{\"start\":-1,\"end\":5}," + tail).has_value()); // negative start
}
// S2 test case 3: the seam-field addition is purely additive — dedup-by-hash, tier
// moves/filtering, and BankIndex ordering are byte-for-byte unchanged by the
// moves/filtering, and BankModel ordering are byte-for-byte unchanged by the
// presence (or absence) of rootNote/loop. Two samples differing ONLY in seam fields
// but sharing a content hash still collapse; a seam-populated sample tiers exactly
// like any other.
static void testSeamFieldsAdditiveInvariant() {
BankIndex idx;
BankModel idx;
Sample a = fullSample("z"); // has rootNote + loop populated
CHECK(idx.add(a) == AddResult::Added);