// Standalone tests for reasampler::wire::bake_wire — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling wire tests. // // Covers: the exact bytes each record encodes to (the two artifacts ship independently, so // a field reorder or an inserted field must fail here rather than pass a round-trip and // break a mixed-version pair); request + outcome round-trips including bytes that would // break a delimiter-based format; the refusals every house wire record shares (wrong tag, // truncation, trailing garbage, a swapped record kind); an unrecognized status integer // degrading to Failed rather than to Ok; and the action lookup name's leading underscore. #include "../src/core/wire/bake_wire.h" #include "../src/core/version/app_version.h" #include #include using namespace reasampler::wire; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) int main() { // --- The exact bytes on the wire ------------------------------------------------- // A round-trip alone would pass a reordered or inserted field; the tags exist to guard // the LAYOUT, so the layout is what is pinned. Changing either literal below means an // already-shipped pair of artifacts can no longer talk — bump the tag, don't edit it. { BakeRequest req; req.instanceGuid = "abcd"; req.stagedFilePath = "T/b.wav"; req.sourceSampleId = "cap-1"; req.sourceRelativePath = "bank/k.wav"; req.sourceDisplayName = "Kick"; req.ownUsageKey = "rsusage_abcd"; req.rootNote = 36; req.generation = 1893456000; CHECK(encodeBakeRequest(req) == "rsbakereq1" "4:abcd" "7:T/b.wav" "5:cap-1" "10:bank/k.wav" "4:Kick" "12:rsusage_abcd" "2:36" "10:1893456000"); BakeOutcome out; out.status = BakeStatus::Ok; out.sampleId = "bake-1"; out.relativePath = "bank/k2.wav"; out.displayName = "Kick r2"; out.rootNote = 36; out.channelCount = 2; out.replaced = true; out.message = "replaced"; out.generation = 1893456000; CHECK(encodeBakeOutcome(out) == "rsbakeout1" "1:0" "6:bake-1" "11:bank/k2.wav" "7:Kick r2" "2:36" "1:2" "1:1" "8:replaced" "10:1893456000"); } // --- Request round-trip, with hostile field content ----------------------------- { BakeRequest req; req.instanceGuid = "0123abcd"; req.stagedFilePath = "C:/Temp/re:sampler 9000/bake 12:34.wav"; // colons + spaces req.sourceSampleId = "cap-1"; req.sourceRelativePath = "reasampler_bank/kick.wav"; req.sourceDisplayName = "Kick r2"; req.ownUsageKey = "rsusage_0123abcd"; req.rootNote = 36; req.generation = 1893456000; const std::string encoded = encodeBakeRequest(req); const auto decoded = decodeBakeRequest(encoded); CHECK(decoded.has_value()); CHECK(*decoded == req); // Empty strings and a zero generation survive too (a first, un-named source). BakeRequest bare; CHECK(decodeBakeRequest(encodeBakeRequest(bare)) == bare); } // --- Outcome round-trip ----------------------------------------------------------- { BakeOutcome out; out.status = BakeStatus::Ok; out.sampleId = "bake-1893456000-kick_1893456000.wav"; out.relativePath = "reasampler_bank/kick_1893456000.wav"; out.displayName = "Kick r3"; out.rootNote = 36; out.channelCount = 2; out.replaced = true; out.message = "replaced the bank entry"; out.generation = 1893456000; const auto decoded = decodeBakeOutcome(encodeBakeOutcome(out)); CHECK(decoded.has_value()); CHECK(*decoded == out); CHECK(decoded->replaced); out.replaced = false; CHECK(decodeBakeOutcome(encodeBakeOutcome(out))->replaced == false); } // --- Malformed input is refused, never half-parsed --------------------------------- { BakeRequest req; req.instanceGuid = "abc"; req.rootNote = 60; const std::string good = encodeBakeRequest(req); CHECK(!decodeBakeRequest("").has_value()); CHECK(!decodeBakeRequest("rsbakereq0" + good.substr(10)).has_value()); // wrong tag CHECK(!decodeBakeRequest(good.substr(0, good.size() - 3)).has_value()); // truncated CHECK(!decodeBakeRequest(good + "junk").has_value()); // trailing // The two records share a key; each must refuse the other's bytes outright. CHECK(!decodeBakeOutcome(good).has_value()); CHECK(!decodeBakeRequest(encodeBakeOutcome(BakeOutcome{})).has_value()); } // --- A status integer this build does not know reads as a FAILURE ------------------ { // Hand-built with a future status value; every other field is well-formed, so only // the vocabulary gap is under test. BakeOutcome out; out.status = BakeStatus::Ok; out.generation = 7; std::string wire = encodeBakeOutcome(out); // The status field is the first after the tag: "':'". const std::string okField = "1:0"; const std::size_t at = wire.find(okField); CHECK(at != std::string::npos); wire.replace(at, okField.size(), "2:99"); const auto decoded = decodeBakeOutcome(wire); CHECK(decoded.has_value()); CHECK(decoded->status == BakeStatus::Failed); // never Ok CHECK(decoded->generation == 7); // Every status this build DOES know survives its own round trip — including the // most recently appended one, which an older reader will see as Failed. for (const BakeStatus s : {BakeStatus::Ok, BakeStatus::Failed, BakeStatus::NoProject, BakeStatus::StagedMissing, BakeStatus::NoSource, BakeStatus::IndexRejected, BakeStatus::WrongProject}) { BakeOutcome one; one.status = s; const auto back = decodeBakeOutcome(encodeBakeOutcome(one)); CHECK(back.has_value() && back->status == s); } } // --- The lookup name carries the underscore the registration string does not ------- // The load-bearing half is the REGISTRATION string: main.cpp registers that spelling // verbatim, and NamedCommandLookup needs exactly one underscore in front of it. If // channelCommandId ever grew one of its own, the lookup would carry two and resolve to // nothing. { const std::string registered = reasampler::version::channelCommandId(kBakeActionSuffix); const std::string lookup = bakeActionLookupName(); const std::size_t suffixLen = std::string(kBakeActionSuffix).size(); CHECK(!registered.empty()); CHECK(registered.front() != '_'); CHECK(lookup.size() == registered.size() + 1); CHECK(lookup.front() == '_' && lookup[1] != '_'); CHECK(lookup.compare(1, std::string::npos, registered) == 0); // The suffix is the TAIL of the id — the channel prefix goes in front of it, and a // suffix that drifted into the middle would name a different action. CHECK(lookup.size() > suffixLen); CHECK(lookup.compare(lookup.size() - suffixLen, suffixLen, kBakeActionSuffix) == 0); } if (g_fail == 0) std::printf("bake_wire: all tests passed\n"); return g_fail ? 1 : 0; }