S17 drop-and-load onto track FX button; S13 editor drop-accept (relay degraded)
S17: drag_out InstrumentDrop gesture + instrument_drop blob reusing the instrument's own serializer; bank_panel FX hover-track + add-VST/vst_chunk inject. S13 relay deferred (read-only bridge) — editor shows drop affordance.
This commit is contained in:
@@ -84,6 +84,54 @@ static void testOffsetPanelRect() {
|
||||
CHECK(decideGesture(100, 19, p, s) == DragGesture::OsDrag); // just above origin
|
||||
}
|
||||
|
||||
// --- S17 InstrumentDrop gesture (single-capture over REAPER UI) ---------------
|
||||
//
|
||||
// M11 REGRESSION GUARD (load-bearing): every M11 case above uses DragState{true, true},
|
||||
// which leaves singleCapture=overReaperUi=false — so an M11-era payload outside the client
|
||||
// rect still decides OsDrag exactly as before. The tests above ARE the M11 non-regression
|
||||
// proof; these add the new middle case.
|
||||
|
||||
// A SINGLE-capture drag that has left the panel but is still over REAPER's own UI is an
|
||||
// instrument drop (heading for a track's FX button), NOT an OS drag.
|
||||
static void testSingleCaptureOverReaperUiIsInstrumentDrop() {
|
||||
DragState s{/*dragging=*/true, /*hasArmedSamples=*/true,
|
||||
/*singleCapture=*/true, /*overReaperUi=*/true};
|
||||
CHECK(decideGesture(500, 150, kPanel, s) == DragGesture::InstrumentDrop); // right of panel
|
||||
CHECK(decideGesture(-5, 150, kPanel, s) == DragGesture::InstrumentDrop); // left of panel
|
||||
CHECK(decideGesture(200, 400, kPanel, s) == DragGesture::InstrumentDrop); // below
|
||||
}
|
||||
|
||||
// InstrumentDrop is an OUTSIDE-only refinement: the same single-capture state INSIDE the
|
||||
// client rect is still the unchanged Internal bank-to-bank drag (invariant #4).
|
||||
static void testSingleCaptureInsidePanelStaysInternal() {
|
||||
DragState s{true, true, /*singleCapture=*/true, /*overReaperUi=*/true};
|
||||
CHECK(decideGesture(200, 150, kPanel, s) == DragGesture::Internal);
|
||||
}
|
||||
|
||||
// A single-capture drag that has left REAPER ENTIRELY (overReaperUi=false) falls through to
|
||||
// OsDrag — the M11 OS drag-out to Explorer/another DAW, unchanged. This is the boundary
|
||||
// refinement's other half: leaving the client rect no longer immediately means OS-bound.
|
||||
static void testSingleCaptureOffReaperIsOsDrag() {
|
||||
DragState s{true, true, /*singleCapture=*/true, /*overReaperUi=*/false};
|
||||
CHECK(decideGesture(500, 150, kPanel, s) == DragGesture::OsDrag);
|
||||
}
|
||||
|
||||
// A MULTI-capture drag over REAPER's UI is REJECTED for InstrumentDrop (the S17 open-question
|
||||
// lean): it is NOT a single instrument placement, so it falls through to OsDrag even while
|
||||
// over REAPER's UI — the multi-file drag-out is the natural gesture for a multi payload.
|
||||
static void testMultiCaptureOverReaperUiIsOsDrag() {
|
||||
DragState s{true, true, /*singleCapture=*/false, /*overReaperUi=*/true};
|
||||
CHECK(decideGesture(500, 150, kPanel, s) == DragGesture::OsDrag);
|
||||
}
|
||||
|
||||
// Not-dragging / no-armed-samples still short-circuits to None regardless of the S17 fields.
|
||||
static void testS17FieldsIgnoredWhenNotDragging() {
|
||||
CHECK(decideGesture(500, 150, kPanel,
|
||||
DragState{false, true, true, true}) == DragGesture::None);
|
||||
CHECK(decideGesture(500, 150, kPanel,
|
||||
DragState{true, false, true, true}) == DragGesture::None);
|
||||
}
|
||||
|
||||
// --- Path-list assembly -------------------------------------------------------
|
||||
|
||||
static ResolvedSample ok(const std::string& p) { return ResolvedSample{p, true}; }
|
||||
@@ -180,6 +228,12 @@ int main() {
|
||||
testReentryReturnsInternal();
|
||||
testOffsetPanelRect();
|
||||
|
||||
testSingleCaptureOverReaperUiIsInstrumentDrop();
|
||||
testSingleCaptureInsidePanelStaysInternal();
|
||||
testSingleCaptureOffReaperIsOsDrag();
|
||||
testMultiCaptureOverReaperUiIsOsDrag();
|
||||
testS17FieldsIgnoredWhenNotDragging();
|
||||
|
||||
testSinglePath();
|
||||
testMultiPreservesOrder();
|
||||
testDedupeSamePath();
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// Standalone tests for reasampler::instrument_drop — no REAPER, no VST3 SDK, no framework.
|
||||
// The S17 drop-and-load blob-construction contract: the extension builds a vst_chunk blob
|
||||
// whose bytes are EXACTLY what ReaSampler 9000's own setState (deserializeComponentState)
|
||||
// accepts, with the dragged capture pre-selected. The round-trip proof (build -> base64
|
||||
// decode -> the instrument's OWN reader -> assert the capture selected) IS the cross-artifact
|
||||
// contract guard — the same pattern assignment_request_tests uses for its wire format.
|
||||
|
||||
#include "../src/instrument_drop.h"
|
||||
#include "../src/vst/sample_map.h" // deserializeComponentState — the instrument's OWN reader
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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)
|
||||
|
||||
// A representative project rate for the reader (the legacy-v3 conversion parameter; our v5
|
||||
// blob never consumes it, but the reader signature requires a positive rate).
|
||||
static constexpr double kRate = 48000.0;
|
||||
|
||||
// THE contract test: a blob built for a capture id decodes — through the instrument's OWN
|
||||
// reader — to a ComponentState with THAT id selected, no zones, default mono. If this fails,
|
||||
// the extension would inject bytes the instrument's setState rejects and the drop would load
|
||||
// a silent/wrong instance.
|
||||
static void testBlobRoundTripsThroughInstrumentReader() {
|
||||
const std::string id = "cap-7f3a-guid";
|
||||
const std::string b64 = buildInstrumentDropChunk(id);
|
||||
CHECK(!b64.empty());
|
||||
|
||||
const std::vector<std::uint8_t> bytes = decodeBase64(b64);
|
||||
CHECK(!bytes.empty());
|
||||
// The base64 must decode to EXACTLY the pre-encode state bytes (no corruption).
|
||||
CHECK(bytes == instrumentDropStateBytes(id));
|
||||
|
||||
const ComponentState cs = deserializeComponentState(bytes, kRate);
|
||||
CHECK(cs.selectionId == id); // the capture IS selected — the whole point
|
||||
CHECK(cs.map.zones.empty()); // a drop selects one capture, authors no zones
|
||||
CHECK(cs.channelMode == ChannelMode::Mono); // fresh-instance default
|
||||
CHECK(cs.lastConsumedAssignGeneration == 0); // fresh instance, no consumed assign
|
||||
}
|
||||
|
||||
// A GUID-shaped id with bytes that would trip a naive delimiter-based encoder round-trips
|
||||
// whole (the length-prefixed component-state framing + base64 carry arbitrary bytes).
|
||||
static void testGuidLikeIdRoundTrips() {
|
||||
const std::string id = "{9A2F0C11-4B6E-4D01-8F3A-0011223344FF}";
|
||||
const std::vector<std::uint8_t> bytes = decodeBase64(buildInstrumentDropChunk(id));
|
||||
const ComponentState cs = deserializeComponentState(bytes, kRate);
|
||||
CHECK(cs.selectionId == id);
|
||||
}
|
||||
|
||||
// An empty id yields the empty-state blob: it still decodes cleanly to {"", no zones} — the
|
||||
// S10 silent empty state. (The shell guards against dropping nothing; the pure contract holds.)
|
||||
static void testEmptyIdYieldsEmptyState() {
|
||||
const std::vector<std::uint8_t> bytes = decodeBase64(buildInstrumentDropChunk(""));
|
||||
CHECK(!bytes.empty()); // still a versioned envelope, just an empty selection
|
||||
const ComponentState cs = deserializeComponentState(bytes, kRate);
|
||||
CHECK(cs.selectionId.empty());
|
||||
CHECK(cs.map.zones.empty());
|
||||
}
|
||||
|
||||
// Deterministic: the same id always produces the same blob (no time/random in the path).
|
||||
static void testDeterministic() {
|
||||
CHECK(buildInstrumentDropChunk("abc") == buildInstrumentDropChunk("abc"));
|
||||
CHECK(buildInstrumentDropChunk("abc") != buildInstrumentDropChunk("abd"));
|
||||
}
|
||||
|
||||
// --- base64 codec unit coverage (the encode side the shell actually ships) -----
|
||||
|
||||
static std::vector<std::uint8_t> b(std::initializer_list<int> v) {
|
||||
std::vector<std::uint8_t> out;
|
||||
for (int x : v) out.push_back(static_cast<std::uint8_t>(x));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Known RFC-4648 vectors, incl. every padding case (0/1/2 trailing bytes).
|
||||
static void testBase64KnownVectors() {
|
||||
CHECK(encodeBase64(b({})) == "");
|
||||
CHECK(encodeBase64(b({'f'})) == "Zg==");
|
||||
CHECK(encodeBase64(b({'f', 'o'})) == "Zm8=");
|
||||
CHECK(encodeBase64(b({'f', 'o', 'o'})) == "Zm9v");
|
||||
CHECK(encodeBase64(b({'f', 'o', 'o', 'b'})) == "Zm9vYg==");
|
||||
CHECK(encodeBase64(b({'f', 'o', 'o', 'b', 'a'})) == "Zm9vYmE=");
|
||||
CHECK(encodeBase64(b({'f', 'o', 'o', 'b', 'a', 'r'})) == "Zm9vYmFy");
|
||||
}
|
||||
|
||||
// encode -> decode is identity across every residue class + all-byte values.
|
||||
static void testBase64RoundTripAllBytes() {
|
||||
for (int len = 0; len <= 300; ++len) {
|
||||
std::vector<std::uint8_t> in;
|
||||
for (int i = 0; i < len; ++i) in.push_back(static_cast<std::uint8_t>((i * 37 + 11) & 0xFF));
|
||||
CHECK(decodeBase64(encodeBase64(in)) == in);
|
||||
}
|
||||
}
|
||||
|
||||
// Malformed decode inputs return empty (never throw / never UB): bad length, illegal char,
|
||||
// misplaced padding.
|
||||
static void testBase64DecodeRejectsMalformed() {
|
||||
CHECK(decodeBase64("Zg=").empty()); // length not a multiple of 4
|
||||
CHECK(decodeBase64("Zm9v!ba=").empty()); // illegal char '!'
|
||||
CHECK(decodeBase64("Z===").empty()); // illegal char in v1 position
|
||||
CHECK(decodeBase64("Zg==Zg==").empty()); // interior padding (pad before the final quad)
|
||||
}
|
||||
|
||||
int main() {
|
||||
testBlobRoundTripsThroughInstrumentReader();
|
||||
testGuidLikeIdRoundTrips();
|
||||
testEmptyIdYieldsEmptyState();
|
||||
testDeterministic();
|
||||
testBase64KnownVectors();
|
||||
testBase64RoundTripAllBytes();
|
||||
testBase64DecodeRejectsMalformed();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user