// Standalone tests for reasampler::vst::bridge_marshal — no VST3, no REAPER, no test // framework. Same fast assert loop as the sibling pure tests: assert the REAPER // bridge-read marshalling (GetProjExtState result decode + a small JSON string-field // reader) directly, so the DAW-facing shell only has to invoke the API. // // Covers: decodeGetProjExtState hit/absent/zero-return/empty-buffer (the stale-buffer // guard); extractJsonStringField present/absent/escapes/whitespace/value-vs-key // disambiguation/non-string-value/malformed. #include "../src/vst/bridge_marshal.h" #include using namespace reasampler::vst; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // --- decodeGetProjExtState ---------------------------------------------------- static void testDecodeHit() { // REAPER reports a non-zero length and filled the buffer: that IS the value. auto v = decodeGetProjExtState(5, "hello"); CHECK(v.has_value()); CHECK(v && *v == "hello"); } static void testDecodeAbsentKey() { // REAPER returns 0 for an absent key. Even if a caller passed a dirty buffer, the // decoder must NOT surface it — the zero return means "no value". auto v = decodeGetProjExtState(0, "stale-bytes-from-a-prior-read"); CHECK(!v.has_value()); } static void testDecodeNegativeReturn() { auto v = decodeGetProjExtState(-1, "whatever"); CHECK(!v.has_value()); } static void testDecodeEmptyBuffer() { // Positive return but empty buffer — treat as no value (defensive). auto v = decodeGetProjExtState(3, ""); CHECK(!v.has_value()); } // --- extractJsonStringField --------------------------------------------------- static void testExtractPresent() { const std::string json = R"({"guid":"ABC-123","name":"kick"})"; auto g = extractJsonStringField(json, "guid"); CHECK(g && *g == "ABC-123"); auto n = extractJsonStringField(json, "name"); CHECK(n && *n == "kick"); } static void testExtractAbsent() { const std::string json = R"({"guid":"ABC-123"})"; CHECK(!extractJsonStringField(json, "missing").has_value()); } static void testExtractWhitespaceTolerant() { const std::string json = "{ \"guid\" : \"X\" , \"n\":\"y\" }"; auto g = extractJsonStringField(json, "guid"); CHECK(g && *g == "X"); } static void testExtractEscapes() { // \" \\ \/ \n \t all decode. const std::string json = R"({"path":"a\\b\/c\"d\ne"})"; auto p = extractJsonStringField(json, "path"); CHECK(p && *p == "a\\b/c\"d\ne"); } static void testExtractValueContainingKeyText() { // A VALUE that contains the key text must not be mistaken for the member. Here the // first "guid" occurrence is inside another value; the real member comes later. const std::string json = R"({"note":"the guid is here","guid":"REAL"})"; auto g = extractJsonStringField(json, "guid"); CHECK(g && *g == "REAL"); } static void testExtractNonStringValue() { // A numeric/object value is not a string — return nullopt rather than garbage. const std::string json = R"({"count":42,"name":"ok"})"; CHECK(!extractJsonStringField(json, "count").has_value()); // The sibling string field still reads. auto n = extractJsonStringField(json, "name"); CHECK(n && *n == "ok"); } static void testExtractMalformed() { CHECK(!extractJsonStringField(R"({"guid":"unterminated)", "guid").has_value()); CHECK(!extractJsonStringField(R"({"guid":)", "guid").has_value()); CHECK(!extractJsonStringField(R"({"guid")", "guid").has_value()); CHECK(!extractJsonStringField("", "guid").has_value()); // Dangling escape at end of string. CHECK(!extractJsonStringField(R"({"guid":"abc\)", "guid").has_value()); } int main() { testDecodeHit(); testDecodeAbsentKey(); testDecodeNegativeReturn(); testDecodeEmptyBuffer(); testExtractPresent(); testExtractAbsent(); testExtractWhitespaceTolerant(); testExtractEscapes(); testExtractValueContainingKeyText(); testExtractNonStringValue(); testExtractMalformed(); if (g_fail == 0) std::printf("bridge_marshal: all tests passed\n"); return g_fail != 0; }