// Standalone tests for reasampler::package::export_plan — no REAPER, no filesystem, // no test framework. The planner's totality claim is the point: every input class // (missing / unreadable / unrepresentable / zero / one) classifies here, and the // names it produces are asserted against the codec's OWN predicates rather than // against a hand-copied rule. #include "../src/core/package/export_plan.h" #include #include #include #include #include #include "../src/core/package/package_format.h" #include "../src/core/package/package_manifest.h" using namespace reasampler::package; using namespace reasampler::model; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // --- fixtures ---------------------------------------------------------------- static Sample sampleAt(const std::string& id, const std::string& relativePath) { Sample s; s.id = id; s.displayName = id + " display"; s.relativePath = relativePath; s.sampleRate = 48000; s.channelCount = 2; s.contentHash = "0123456789abcdef"; return s; } static ExportCandidate present(const std::string& id, const std::string& rel) { return ExportCandidate{sampleAt(id, rel), SourceFileState::Present}; } static ExportCandidate withState(const std::string& id, const std::string& rel, SourceFileState state) { return ExportCandidate{sampleAt(id, rel), state}; } static ExportInputs bankOf(std::vector candidates) { ExportInputs in; in.bankDisplayName = "Drums"; in.candidates = std::move(candidates); std::vector ids; for (const ExportCandidate& c : in.candidates) ids.push_back(c.sample.id); in.slots.resetDense(ids); return in; } static bool hasExclusion(const ExportPlan& p, const std::string& id, ExclusionReason why) { for (const ExcludedEntry& e : p.excluded) if (e.sampleId == id && e.reason == why) return true; return false; } // --- the four classification inputs the plan names --------------------------- static void testZeroSamplesIsAReadyEmptyPlan() { const ExportPlan p = planExport(bankOf({})); CHECK(p.verdict == ExportVerdict::Ready); CHECK(p.manifest.entries.empty()); CHECK(p.sourceRelativePaths.empty()); CHECK(p.excluded.empty()); CHECK(p.manifest.bankDisplayName == "Drums"); CHECK(p.manifest.slots.empty()); } static void testOneSamplePresentShips() { const ExportPlan p = planExport(bankOf({present("s1", "reasampler_bank/kick.wav")})); CHECK(p.verdict == ExportVerdict::Ready); CHECK(p.manifest.entries.size() == 1); CHECK(p.excluded.empty()); CHECK(p.manifest.entries[0].fileName == "kick.wav"); CHECK(p.manifest.entries[0].sample.id == "s1"); // The source spelling survives only on the side channel; the transport record // names the payload by its bare package name. CHECK(p.sourceRelativePaths.size() == 1); CHECK(p.sourceRelativePaths[0] == "reasampler_bank/kick.wav"); CHECK(p.manifest.entries[0].sample.relativePath == "kick.wav"); } static void testMissingFileIsIncompleteNotRefused() { const ExportPlan p = planExport(bankOf({ present("s1", "reasampler_bank/kick.wav"), withState("s2", "reasampler_bank/gone.wav", SourceFileState::Missing), })); CHECK(p.verdict == ExportVerdict::Incomplete); CHECK(p.manifest.entries.size() == 1); CHECK(p.manifest.entries[0].sample.id == "s1"); CHECK(p.excluded.size() == 1); CHECK(hasExclusion(p, "s2", ExclusionReason::FileMissing)); CHECK(p.excluded[0].relativePath == "reasampler_bank/gone.wav"); } static void testUnreadableFileStaysDistinctFromMissing() { const ExportPlan p = planExport(bankOf({ withState("s1", "reasampler_bank/locked.wav", SourceFileState::Unreadable), withState("s2", "reasampler_bank/gone.wav", SourceFileState::Missing), })); CHECK(p.verdict == ExportVerdict::Incomplete); CHECK(p.manifest.entries.empty()); CHECK(p.excluded.size() == 2); CHECK(hasExclusion(p, "s1", ExclusionReason::FileUnreadable)); CHECK(hasExclusion(p, "s2", ExclusionReason::FileMissing)); CHECK(!hasExclusion(p, "s1", ExclusionReason::FileMissing)); } static void testUnrepresentableRecordRefusesWholeExport() { // A traversing nested path — the one thing an index record can carry that // BankModel::add does not itself refuse. const ExportPlan traversal = planExport(bankOf({present("s1", "reasampler_bank/kick.wav"), present("s2", "reasampler_bank/../evil.wav")})); CHECK(traversal.verdict == ExportVerdict::Refused); CHECK(hasExclusion(traversal, "s2", ExclusionReason::RecordUnrepresentable)); CHECK(traversal.manifest.entries.size() == 1); // still reports what WOULD ship const ExportPlan emptyId = planExport(bankOf({present("", "reasampler_bank/kick.wav")})); CHECK(emptyId.verdict == ExportVerdict::Refused); const ExportPlan absolute = planExport(bankOf({present("s1", "C:/elsewhere/kick.wav")})); CHECK(absolute.verdict == ExportVerdict::Refused); CHECK(hasExclusion(absolute, "s1", ExclusionReason::RecordUnrepresentable)); // Refused outranks Incomplete: a corrupt record is not something the // "export the present N" confirm can proceed past. const ExportPlan both = planExport(bankOf({ withState("s1", "reasampler_bank/gone.wav", SourceFileState::Missing), present("s2", "reasampler_bank/../evil.wav"), })); CHECK(both.verdict == ExportVerdict::Refused); } // --- transport names ---------------------------------------------------------- static void testHostileNamesAreRepairedNotRelayed() { // Every one of these is a name the codec refuses and a filesystem somewhere // produces honestly. const std::vector hostile = { "reasampler_bank/ki:ck?.wav", "reasampler_bank/a|bd\"e*f.wav", "reasampler_bank/CON.wav", "reasampler_bank/nul", "reasampler_bank/trailing .wav ", "reasampler_bank/dots...", // A literal ".." COMPONENT is not a name to repair — it is a traversing // record, and the Refused test above owns it. "reasampler_bank/.....", "reasampler_bank/.", std::string("reasampler_bank/bad\xC3.wav"), // truncated UTF-8 sequence std::string("reasampler_bank/") + std::string(400, 'x') + ".wav", }; std::vector candidates; for (std::size_t i = 0; i < hostile.size(); ++i) candidates.push_back(present("s" + std::to_string(i), hostile[i])); const ExportPlan p = planExport(bankOf(candidates)); CHECK(p.verdict == ExportVerdict::Ready); CHECK(p.manifest.entries.size() == hostile.size()); for (const PackageEntry& e : p.manifest.entries) { CHECK(isValidEntryName(e.fileName)); CHECK(isValidNestedSamplePath(e.sample.relativePath)); } } static void testUniqueNameSurvivesLongExtensionUnderflow() { // insertSuffix computes room = kMaxEntryNameBytes - suffix.size() - ext.size() in // size_t; an extension long enough that even a two-digit "_10" suffix pushes the // sum past the cap must not wrap that subtraction. Ten same-named entries force // the tenth collision into double digits against a 253-byte extension (253 + 3 = // 256, one over kMaxEntryNameBytes). const std::string hostileName = "a." + std::string(252, 'x'); // 254 bytes, otherwise valid std::vector candidates; for (int i = 0; i < 10; ++i) candidates.push_back(present("s" + std::to_string(i), "reasampler_bank/" + hostileName)); const ExportPlan p = planExport(bankOf(candidates)); CHECK(p.verdict == ExportVerdict::Ready); CHECK(p.manifest.entries.size() == 10); for (const PackageEntry& e : p.manifest.entries) CHECK(isValidEntryName(e.fileName)); for (std::size_t i = 0; i < p.manifest.entries.size(); ++i) for (std::size_t j = i + 1; j < p.manifest.entries.size(); ++j) CHECK(!sameEntryName(p.manifest.entries[i].fileName, p.manifest.entries[j].fileName)); } static void testCaseFoldedCollisionsAreDisambiguated() { const ExportPlan p = planExport(bankOf({ present("s1", "reasampler_bank/Kick.wav"), present("s2", "reasampler_bank/kick.wav"), present("s3", "reasampler_bank/KICK.wav"), })); CHECK(p.verdict == ExportVerdict::Ready); CHECK(p.manifest.entries.size() == 3); for (std::size_t i = 0; i < p.manifest.entries.size(); ++i) for (std::size_t j = i + 1; j < p.manifest.entries.size(); ++j) CHECK(!sameEntryName(p.manifest.entries[i].fileName, p.manifest.entries[j].fileName)); CHECK(p.manifest.entries[0].fileName == "Kick.wav"); // The suffix goes before the extension, so the payload keeps its type. CHECK(p.manifest.entries[1].fileName == "kick_2.wav"); } static void testSanitizeNeverReturnsANameTheCodecRefuses() { const std::vector raws = { "", ".", "..", "...", " ", "com1", "LPT9.WAV", "a/b", "a\\b", "C:evil", std::string("\x01\x02\x03"), std::string(300, 'y'), std::string("caf\xC3\xA9.wav"), // well-formed UTF-8 must survive intact }; for (const std::string& raw : raws) CHECK(isValidEntryName(sanitizeEntryName(raw))); CHECK(sanitizeEntryName("caf\xC3\xA9.wav") == "caf\xC3\xA9.wav"); CHECK(sanitizeEntryName("kick.wav") == "kick.wav"); } // --- what the plan hands the codec ------------------------------------------- static void testPlannedManifestSatisfiesTheCodec() { ExportPlan p = planExport(bankOf({ present("s1", "reasampler_bank/Kick.wav"), present("s2", "reasampler_bank/kick.wav"), present("s3", "reasampler_bank/CON.wav"), present("s4", std::string("reasampler_bank/caf\xC3\xA9 mix.wav")), })); // byteLength/byteHash are the shell's to measure; stand them in so the encode // path under test is the naming, not the digest. for (PackageEntry& e : p.manifest.entries) { e.byteLength = 44; e.byteHash = "aaaaaaaabbbbbbbb"; } const std::optional json = serializeManifest(p.manifest); CHECK(json.has_value()); if (json) { const std::optional back = deserializeManifest(*json); CHECK(back.has_value()); if (back) CHECK(*back == p.manifest); } } static void testSlotsFollowMembership() { ExportInputs in = bankOf({ present("s1", "reasampler_bank/a.wav"), withState("s2", "reasampler_bank/gone.wav", SourceFileState::Missing), present("s3", "reasampler_bank/c.wav"), }); const ExportPlan p = planExport(in); CHECK(p.manifest.slots.slotOf("s2") == -1); // an excluded id keeps no display position CHECK(p.manifest.slots.slotOf("s1") >= 0); CHECK(p.manifest.slots.slotOf("s3") >= 0); CHECK(p.manifest.slots.size() == 2); } static void testPlanIsDeterministic() { const ExportInputs in = bankOf({ present("s1", "reasampler_bank/Kick.wav"), present("s2", "reasampler_bank/kick.wav"), withState("s3", "reasampler_bank/gone.wav", SourceFileState::Missing), }); const ExportPlan a = planExport(in); const ExportPlan b = planExport(in); CHECK(a.verdict == b.verdict); CHECK(a.manifest == b.manifest); CHECK(a.sourceRelativePaths == b.sourceRelativePaths); CHECK(a.excluded.size() == b.excluded.size()); } int main() { testZeroSamplesIsAReadyEmptyPlan(); testOneSamplePresentShips(); testMissingFileIsIncompleteNotRefused(); testUnreadableFileStaysDistinctFromMissing(); testUnrepresentableRecordRefusesWholeExport(); testHostileNamesAreRepairedNotRelayed(); testUniqueNameSurvivesLongExtensionUnderflow(); testCaseFoldedCollisionsAreDisambiguated(); testSanitizeNeverReturnsANameTheCodecRefuses(); testPlannedManifestSatisfiesTheCodec(); testSlotsFollowMembership(); testPlanIsDeterministic(); if (g_fail == 0) { std::printf("export_plan_tests: all passed\n"); return 0; } std::printf("export_plan_tests: %d failure(s)\n", g_fail); return 1; }