// Standalone tests for reasampler::AssignmentRequest — no REAPER, no framework. // The S8 ingest assignment-request seam: the (bankId, sampleId, generation) value the // extension writes to ext-state after an ingest-with-assign, decoded by the instrument // in a later dispatch. Only the wire format lives in this module; test it hard because // the reader (a different artifact) must decode exactly what this writer produces. // // Covers: encode/decode round-trip, ids carrying arbitrary bytes (GUIDs, separators), // the generation field including zero and negative-guard, and malformed/truncated/ // trailing-garbage input -> nullopt (the reader's "no pending request" fallback hinges // on it). #include "../src/assignment_request.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) // --- round-trip -------------------------------------------------------------- static void testRoundTrip() { AssignmentRequest req; req.bankId = "{12345678-1234-1234-1234-1234567890AB}"; req.sampleId = "cap-1700000000-kick.wav"; req.generation = 1700000123; const std::string wire = encodeAssignmentRequest(req); auto back = decodeAssignmentRequest(wire); CHECK(back.has_value()); CHECK(*back == req); // Re-encoding the decoded value is byte-stable (deterministic encoder). CHECK(encodeAssignmentRequest(*back) == wire); } // The pool bank id and an empty-ish generation must round-trip too (generation 0 is the // documented pre-S9 default; an assign still carries a real stamp, but 0 must be legal). static void testRoundTripPoolAndZeroGeneration() { AssignmentRequest req; req.bankId = "pool"; req.sampleId = "s1"; req.generation = 0; auto back = decodeAssignmentRequest(encodeAssignmentRequest(req)); CHECK(back.has_value()); CHECK(back->bankId == "pool"); CHECK(back->sampleId == "s1"); CHECK(back->generation == 0); } // Ids carrying the wire's own metacharacters (':' the length delimiter, digits that // could be misread as a length, the magic-tag bytes) must survive whole — the whole // reason for length-prefixing over a delimiter-split format. static void testRoundTripAdversarialIds() { AssignmentRequest req; req.bankId = "12:34:has-colons"; // ':' is the length delimiter req.sampleId = "rsassign1-lookalike-99"; // embeds the magic tag req.generation = -42; // negative is representable auto back = decodeAssignmentRequest(encodeAssignmentRequest(req)); CHECK(back.has_value()); CHECK(*back == req); CHECK(back->bankId == "12:34:has-colons"); CHECK(back->sampleId == "rsassign1-lookalike-99"); CHECK(back->generation == -42); } // Empty ids are structurally valid on the wire (length 0) and must round-trip — the // decoder must not conflate an empty field with a parse failure. static void testRoundTripEmptyFields() { AssignmentRequest req; req.bankId = ""; req.sampleId = ""; req.generation = 7; auto back = decodeAssignmentRequest(encodeAssignmentRequest(req)); CHECK(back.has_value()); CHECK(*back == req); } // A large generation (past 32-bit) must not truncate — the field is int64. static void testLargeGeneration() { AssignmentRequest req; req.bankId = "b"; req.sampleId = "s"; req.generation = 9007199254740993LL; // > 2^53, > INT32_MAX auto back = decodeAssignmentRequest(encodeAssignmentRequest(req)); CHECK(back.has_value()); CHECK(back->generation == 9007199254740993LL); } // --- malformed / tolerant parse ---------------------------------------------- static void testMalformedParse() { // Absence / total garbage — the reader maps these to "no pending request". CHECK(!decodeAssignmentRequest("").has_value()); CHECK(!decodeAssignmentRequest("not a request").has_value()); // Wrong magic tag. CHECK(!decodeAssignmentRequest("rsprov1" "1:b1:s1:7").has_value()); // Magic only, no fields. CHECK(!decodeAssignmentRequest("rsassign1").has_value()); // Truncated mid-record (missing the generation field). CHECK(!decodeAssignmentRequest("rsassign1" "4:pool2:s1").has_value()); // A length that runs past the end. CHECK(!decodeAssignmentRequest("rsassign1" "99:short").has_value()); // Non-numeric length token. CHECK(!decodeAssignmentRequest("rsassign1" "x:pool2:s11:7").has_value()); // A non-numeric generation field. CHECK(!decodeAssignmentRequest("rsassign1" "4:pool2:s13:abc").has_value()); // A bare "-" generation. CHECK(!decodeAssignmentRequest("rsassign1" "4:pool2:s11:-").has_value()); } // Trailing garbage after a well-formed record must be rejected — a partial/padded blob // is not a valid request, and the reader must never accept the prefix and ignore the rest. static void testTrailingGarbageRejected() { AssignmentRequest req; req.bankId = "pool"; req.sampleId = "s1"; req.generation = 7; const std::string wire = encodeAssignmentRequest(req); // The clean value parses. CHECK(decodeAssignmentRequest(wire).has_value()); // The same value with any trailing byte does not. CHECK(!decodeAssignmentRequest(wire + "X").has_value()); CHECK(!decodeAssignmentRequest(wire + "0:").has_value()); } int main() { testRoundTrip(); testRoundTripPoolAndZeroGeneration(); testRoundTripAdversarialIds(); testRoundTripEmptyFields(); testLargeGeneration(); testMalformedParse(); testTrailingGarbageRejected(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }