Files
reasampler/tests/test_owned_manifest.cpp
T
daniel 63f35fa58d feat(persist): owned-file manifest seam — capture records created files (B-cap)
Pure OwnedFileManifest (relative paths, dedup, JSON round-trip) persisted
under sibling owned_files ext-state key; both capture commit paths record;
joins the R-B undo-reload set. Phase R prune consumes it later.
2026-07-26 14:50:18 -04:00

172 lines
6.9 KiB
C++

// Standalone tests for reasampler::OwnedFileManifest — no REAPER, no framework.
// The owned-file manifest seam (Phase B B-cap): a deduplicated, insertion-ordered
// set of project-relative files the capture path created, with JSON round-trip.
//
// Covers (brief-named): JSON round-trip, dedup of repeated adds, the empty manifest.
// Plus: the relative-paths-only invariant (reject empty / absolute), contains()
// semantics, insertion-order preservation, malformed-parse -> nullopt (the persist
// shell's warn+fallback hinges on it), and round-trip of paths with JSON metacharacters.
#include "../src/owned_manifest.h"
#include <cstdio>
#include <string>
using namespace reasampler;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// --- empty manifest ----------------------------------------------------------
static void testEmptyManifest() {
OwnedFileManifest m;
CHECK(m.empty());
CHECK(m.size() == 0);
CHECK(m.paths().empty());
CHECK(!m.contains("anything.wav"));
// An empty manifest serializes to a well-formed shape and round-trips to empty.
const std::string json = m.serialize();
auto back = OwnedFileManifest::deserialize(json);
CHECK(back.has_value());
CHECK(*back == m);
CHECK(back->empty());
}
// --- add / contains / order --------------------------------------------------
static void testAddAndContains() {
OwnedFileManifest m;
CHECK(m.add("reasampler_bank/a.wav") == ManifestAddResult::Added);
CHECK(m.add("reasampler_bank/b.wav") == ManifestAddResult::Added);
CHECK(m.size() == 2);
CHECK(m.contains("reasampler_bank/a.wav"));
CHECK(m.contains("reasampler_bank/b.wav"));
CHECK(!m.contains("reasampler_bank/c.wav"));
// Exact-string match — not a prefix / substring match.
CHECK(!m.contains("reasampler_bank/a"));
CHECK(!m.contains("a.wav"));
// Insertion order is preserved (deterministic ext-state).
CHECK(m.paths().size() == 2);
CHECK(m.paths()[0] == "reasampler_bank/a.wav");
CHECK(m.paths()[1] == "reasampler_bank/b.wav");
}
// --- dedup of repeated adds --------------------------------------------------
static void testDedupRepeatedAdds() {
OwnedFileManifest m;
CHECK(m.add("reasampler_bank/take.wav") == ManifestAddResult::Added);
// A repeat capture of an identical request must not double-record the file.
CHECK(m.add("reasampler_bank/take.wav") == ManifestAddResult::AlreadyPresent);
CHECK(m.add("reasampler_bank/take.wav") == ManifestAddResult::AlreadyPresent);
CHECK(m.size() == 1);
CHECK(m.paths().size() == 1);
}
// --- relative-paths-only invariant -------------------------------------------
static void testRejectsEmptyAndAbsolute() {
OwnedFileManifest m;
CHECK(m.add("") == ManifestAddResult::RejectedEmptyPath);
// Every absolute form bank_model rejects, the manifest rejects too.
CHECK(m.add("/abs/take.wav") == ManifestAddResult::RejectedAbsolutePath); // POSIX root
CHECK(m.add("\\\\host\\share\\x.wav") == ManifestAddResult::RejectedAbsolutePath); /* UNC */
CHECK(m.add("C:/bank/x.wav") == ManifestAddResult::RejectedAbsolutePath); // Win drive /
CHECK(m.add("C:\\bank\\x.wav") == ManifestAddResult::RejectedAbsolutePath); /* Win drive backslash */
CHECK(m.add("C:x.wav") == ManifestAddResult::RejectedAbsolutePath); // drive-relative
// A rejected add never mutates.
CHECK(m.empty());
CHECK(!m.contains("/abs/take.wav"));
}
// --- JSON round-trip ---------------------------------------------------------
static void testRoundTrip() {
OwnedFileManifest m;
m.add("reasampler_bank/one.wav");
m.add("reasampler_bank/two.wav");
m.add("reasampler_bank/three.wav");
const std::string json = m.serialize();
auto back = OwnedFileManifest::deserialize(json);
CHECK(back.has_value());
CHECK(*back == m);
// Order + membership survive.
CHECK(back->paths().size() == 3);
CHECK(back->paths()[0] == "reasampler_bank/one.wav");
CHECK(back->paths()[2] == "reasampler_bank/three.wav");
// serialize(deserialize(serialize(x))) is stable.
CHECK(back->serialize() == json);
}
// A path carrying JSON metacharacters must survive the escape/unescape round-trip.
static void testRoundTripEscaping() {
OwnedFileManifest m;
m.add("reasampler_bank/od\"d name.wav"); // embedded quote
m.add("reasampler_bank/back\\slash.wav"); // embedded backslash
m.add("reasampler_bank/tab\tafter.wav"); // control char
auto back = OwnedFileManifest::deserialize(m.serialize());
CHECK(back.has_value());
CHECK(*back == m);
CHECK(back->contains("reasampler_bank/od\"d name.wav"));
CHECK(back->contains("reasampler_bank/back\\slash.wav"));
CHECK(back->contains("reasampler_bank/tab\tafter.wav"));
}
// --- malformed / tolerant parse ----------------------------------------------
static void testMalformedParse() {
// The persist shell's warn+fallback hinges on nullopt for a corrupt blob.
CHECK(!OwnedFileManifest::deserialize("").has_value()); // empty string
CHECK(!OwnedFileManifest::deserialize("not json").has_value());
CHECK(!OwnedFileManifest::deserialize("{\"owned\":[").has_value()); // unterminated array
CHECK(!OwnedFileManifest::deserialize("{\"owned\":[1,2]}").has_value()); // non-string element
CHECK(!OwnedFileManifest::deserialize("{\"owned\":\"x\"}").has_value()); // wrong value type
// An explicit empty array parses to an empty manifest.
auto empty = OwnedFileManifest::deserialize("{\"owned\":[]}");
CHECK(empty.has_value());
CHECK(empty->empty());
// An unknown sibling key is tolerated (forward-compat) — the owned array still loads.
auto fwd = OwnedFileManifest::deserialize(
"{\"future\":{\"nested\":[1,2]},\"owned\":[\"reasampler_bank/x.wav\"]}");
CHECK(fwd.has_value());
CHECK(fwd->size() == 1);
CHECK(fwd->contains("reasampler_bank/x.wav"));
// A stored blob cannot smuggle a duplicate or absolute path past the load-time
// invariant re-assertion (deserialize routes each element through add()).
auto dupe = OwnedFileManifest::deserialize(
"{\"owned\":[\"reasampler_bank/x.wav\",\"reasampler_bank/x.wav\"]}");
CHECK(dupe.has_value());
CHECK(dupe->size() == 1);
auto absolute = OwnedFileManifest::deserialize(
"{\"owned\":[\"reasampler_bank/ok.wav\",\"/etc/evil.wav\"]}");
CHECK(absolute.has_value());
CHECK(absolute->size() == 1);
CHECK(absolute->contains("reasampler_bank/ok.wav"));
CHECK(!absolute->contains("/etc/evil.wav"));
}
int main() {
testEmptyManifest();
testAddAndContains();
testDedupRepeatedAdds();
testRejectsEmptyAndAbsolute();
testRoundTrip();
testRoundTripEscaping();
testMalformedParse();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}