feat(vst): stand up VST3 instrument spike (S1) — skeleton, IPlugView↔LICE editor, REAPER bridge read
Vendor Steinberg VST3 SDK (v3.7.9_build_61); add reasampler_vst.vst3 as a second, additive build artifact with pure editor-geometry + bridge-marshal helpers under CTest.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// 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 <cstdio>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Standalone tests for reasampler::vst::editor_geometry — no VST3, no REAPER, no test
|
||||
// framework. Same fast assert loop as the sibling pure tests (mode_switch et al.):
|
||||
// assert the IPlugView LICE editor's layout math + hit-testing directly.
|
||||
//
|
||||
// Covers: contains() half-open convention + degenerate rects; layoutEditor regions on a
|
||||
// normal view (title band + button + canvas), a tiny view (button clamped to canvas,
|
||||
// never overhanging), and a zero view (all rects empty, no inversion); hitTest hitting
|
||||
// the button, missing on the title/canvas, missing outside the surface, and boundary
|
||||
// pixels; layout<->hit-test agreement (a click on the drawn button rect hits it).
|
||||
|
||||
#include "../src/vst/editor_geometry.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
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)
|
||||
|
||||
// --- contains() ---------------------------------------------------------------
|
||||
|
||||
static void testContainsHalfOpen() {
|
||||
Rect r{10, 20, 50, 40}; // [10,50) x [20,40)
|
||||
CHECK(contains(r, 10, 20)); // top-left inclusive
|
||||
CHECK(contains(r, 49, 39)); // bottom-right exclusive edge, inside
|
||||
CHECK(!contains(r, 50, 30)); // right edge excluded
|
||||
CHECK(!contains(r, 30, 40)); // bottom edge excluded
|
||||
CHECK(!contains(r, 9, 30)); // left of rect
|
||||
CHECK(!contains(r, 30, 19)); // above rect
|
||||
}
|
||||
|
||||
static void testContainsDegenerate() {
|
||||
CHECK(!contains(Rect{10, 10, 10, 20}, 10, 15)); // zero width
|
||||
CHECK(!contains(Rect{10, 10, 20, 10}, 15, 10)); // zero height
|
||||
CHECK(!contains(Rect{20, 10, 10, 20}, 15, 15)); // inverted (right < left)
|
||||
}
|
||||
|
||||
// --- layoutEditor: normal view ------------------------------------------------
|
||||
|
||||
static void testLayoutNormalView() {
|
||||
// A comfortable 400x260 view: title band spans the top full width; canvas is the
|
||||
// rest; button sits inside the canvas, inset by the margin.
|
||||
const EditorLayout L = layoutEditor(400, 260);
|
||||
|
||||
CHECK(L.titleBar.left == 0 && L.titleBar.top == 0);
|
||||
CHECK(L.titleBar.right == 400);
|
||||
CHECK(L.titleBar.height() > 0 && L.titleBar.height() <= 260);
|
||||
|
||||
// Canvas begins right below the title bar and reaches the bottom-right.
|
||||
CHECK(L.canvas.top == L.titleBar.bottom);
|
||||
CHECK(L.canvas.right == 400 && L.canvas.bottom == 260);
|
||||
|
||||
// Button is inside the canvas (does not overhang any edge).
|
||||
CHECK(L.button.left >= L.canvas.left);
|
||||
CHECK(L.button.top >= L.canvas.top);
|
||||
CHECK(L.button.right <= L.canvas.right);
|
||||
CHECK(L.button.bottom <= L.canvas.bottom);
|
||||
CHECK(L.button.width() > 0 && L.button.height() > 0);
|
||||
}
|
||||
|
||||
// --- layoutEditor: tiny view (clamping) ---------------------------------------
|
||||
|
||||
static void testLayoutTinyViewClampsButton() {
|
||||
// A view narrower/shorter than the button's natural size: the button must clamp to
|
||||
// the canvas and never produce an inverted or overhanging rect.
|
||||
const EditorLayout L = layoutEditor(40, 40);
|
||||
CHECK(L.button.right <= L.canvas.right);
|
||||
CHECK(L.button.bottom <= L.canvas.bottom);
|
||||
CHECK(L.button.right >= L.button.left); // never inverted
|
||||
CHECK(L.button.bottom >= L.button.top);
|
||||
// Title bar clamps to the client height when the view is shorter than its height.
|
||||
CHECK(L.titleBar.bottom <= 40);
|
||||
}
|
||||
|
||||
// --- layoutEditor: zero view (all empty, no inversion) ------------------------
|
||||
|
||||
static void testLayoutZeroView() {
|
||||
const EditorLayout L = layoutEditor(0, 0);
|
||||
CHECK(L.titleBar.width() <= 0 || L.titleBar.height() <= 0);
|
||||
CHECK(L.canvas.width() <= 0 || L.canvas.height() <= 0);
|
||||
// No rect is inverted.
|
||||
CHECK(L.button.right >= L.button.left);
|
||||
CHECK(L.button.bottom >= L.button.top);
|
||||
CHECK(L.canvas.right >= L.canvas.left);
|
||||
CHECK(L.canvas.bottom >= L.canvas.top);
|
||||
// A click anywhere on an empty layout hits nothing.
|
||||
CHECK(hitTest(L, 0, 0) == HitTarget::kNone);
|
||||
CHECK(hitTest(L, 5, 5) == HitTarget::kNone);
|
||||
}
|
||||
|
||||
// --- hitTest ------------------------------------------------------------------
|
||||
|
||||
static void testHitTestButton() {
|
||||
const EditorLayout L = layoutEditor(400, 260);
|
||||
// Center of the button hits it.
|
||||
const int cx = (L.button.left + L.button.right) / 2;
|
||||
const int cy = (L.button.top + L.button.bottom) / 2;
|
||||
CHECK(hitTest(L, cx, cy) == HitTarget::kButton);
|
||||
}
|
||||
|
||||
static void testHitTestMissesNonButton() {
|
||||
const EditorLayout L = layoutEditor(400, 260);
|
||||
// Title bar is inert in the spike.
|
||||
CHECK(hitTest(L, 200, L.titleBar.top + 1) == HitTarget::kNone);
|
||||
// Empty canvas away from the button.
|
||||
CHECK(hitTest(L, 380, 240) == HitTarget::kNone);
|
||||
// Outside the surface entirely.
|
||||
CHECK(hitTest(L, -5, -5) == HitTarget::kNone);
|
||||
CHECK(hitTest(L, 500, 500) == HitTarget::kNone);
|
||||
}
|
||||
|
||||
static void testHitTestButtonBoundary() {
|
||||
const EditorLayout L = layoutEditor(400, 260);
|
||||
// Top-left corner of the button is inclusive; the right/bottom edges are excluded.
|
||||
CHECK(hitTest(L, L.button.left, L.button.top) == HitTarget::kButton);
|
||||
CHECK(hitTest(L, L.button.right, L.button.top) == HitTarget::kNone);
|
||||
CHECK(hitTest(L, L.button.left, L.button.bottom) == HitTarget::kNone);
|
||||
}
|
||||
|
||||
// --- layout<->hit-test agreement ----------------------------------------------
|
||||
|
||||
// Every pixel inside the drawn button rect must hit the button; this is the
|
||||
// load-bearing consistency invariant between what the shell draws and what it routes.
|
||||
static void testHitTestMatchesDrawnButton() {
|
||||
const EditorLayout L = layoutEditor(320, 200);
|
||||
for (int y = L.button.top; y < L.button.bottom; ++y) {
|
||||
for (int x = L.button.left; x < L.button.right; ++x) {
|
||||
CHECK(hitTest(L, x, y) == HitTarget::kButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
testContainsHalfOpen();
|
||||
testContainsDegenerate();
|
||||
testLayoutNormalView();
|
||||
testLayoutTinyViewClampsButton();
|
||||
testLayoutZeroView();
|
||||
testHitTestButton();
|
||||
testHitTestMissesNonButton();
|
||||
testHitTestButtonBoundary();
|
||||
testHitTestMatchesDrawnButton();
|
||||
|
||||
if (g_fail == 0) std::printf("editor_geometry: all tests passed\n");
|
||||
return g_fail != 0;
|
||||
}
|
||||
Reference in New Issue
Block a user