// 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 #include 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; }