// Standalone tests for reasampler::instrument_drop — no REAPER, no VST3 SDK, no framework. // The S17 drop-and-load payload contract (S-GA-DropFX revision): the extension builds a // Steinberg-format .vstpreset image whose 'Comp' chunk is EXACTLY what ReaSampler 9000's own // setState (deserializeComponentState) accepts, with the dragged capture pre-selected, and // whose header carries the channel-active class ID. The round-trip proof (build -> parse the // container -> the instrument's OWN reader -> assert the capture selected) IS the // cross-artifact contract guard — the same pattern assignment_request_tests uses. #include "../src/core/wire/instrument_drop.h" #include "../src/core/instrument/map/component_state_io.h" // deserializeComponentState — the instrument's OWN reader #include "version_generated.h" // REASAMPLER_CHANNEL_IS_BETA — pins the per-channel class ID #include #include #include #include using namespace reasampler; using namespace reasampler::wire; using namespace reasampler::instrument::map; // ComponentState + the codec (Q-W2v re-namespace) 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 // current-version blob never consumes it, but the reader signature requires a positive rate). static constexpr double kRate = 48000.0; // --- A tiny independent .vstpreset reader (test-local, little-endian) ---------- // Mirrors public.sdk/source/vst/vstpresetfile.cpp's READ side so the builder is proven // against an independent decode, not against itself. static std::uint32_t readU32LE(const std::vector& b, std::size_t at) { return static_cast(b[at]) | (static_cast(b[at + 1]) << 8) | (static_cast(b[at + 2]) << 16) | (static_cast(b[at + 3]) << 24); } static std::uint64_t readU64LE(const std::vector& b, std::size_t at) { std::uint64_t v = 0; for (int i = 7; i >= 0; --i) v = (v << 8) | b[at + static_cast(i)]; return v; } static bool fourCCAt(const std::vector& b, std::size_t at, const char* id) { return b.size() >= at + 4 && b[at] == static_cast(id[0]) && b[at + 1] == static_cast(id[1]) && b[at + 2] == static_cast(id[2]) && b[at + 3] == static_cast(id[3]); } struct ParsedPreset { bool ok = false; std::string classId; std::vector compChunk; }; static ParsedPreset parsePreset(const std::vector& b) { ParsedPreset p; if (b.size() < 48) return p; if (!fourCCAt(b, 0, "VST3")) return p; // header magic if (readU32LE(b, 4) != 1) return p; // kFormatVersion p.classId.assign(b.begin() + 8, b.begin() + 40); // 32-char ASCII class ID const std::uint64_t listOffset = readU64LE(b, 40); if (listOffset + 8 > b.size()) return p; if (!fourCCAt(b, static_cast(listOffset), "List")) return p; const std::uint32_t count = readU32LE(b, static_cast(listOffset) + 4); std::size_t at = static_cast(listOffset) + 8; for (std::uint32_t i = 0; i < count; ++i, at += 20) { if (at + 20 > b.size()) return p; const std::uint64_t off = readU64LE(b, at + 4); const std::uint64_t size = readU64LE(b, at + 12); if (off + size > b.size()) return p; if (fourCCAt(b, at, "Comp")) { p.compChunk.assign(b.begin() + static_cast(off), b.begin() + static_cast(off + size)); p.ok = true; } } return p; } // --- class-ID derivation ------------------------------------------------------ // The class-ID string is pinned PER CHANNEL to the FROZEN UID rendered as FUID::toString // would render it (the four INLINE_UID words as %08X in order — platform-stable, see // instrument_drop.h). These literals are derived independently from the frozen constants in // reasampler_uid.h; if this fails, the .vstpreset would address a class the loaded VST does // not present and TrackFX_SetPreset would refuse the drop. static void testClassIdHexPinnedPerChannel() { #if REASAMPLER_CHANNEL_IS_BETA CHECK(vstClassIdHex() == "CCFFEB3A4FF532A69E1817984256955F"); #else CHECK(vstClassIdHex() == "5E45A11E9C7B4D6AB1E3F2084A6C1D9F"); #endif CHECK(vstClassIdHex().size() == 32); } // --- the drop payload contract ------------------------------------------------ // THE contract test: a preset built for a capture id parses as a well-formed VST3 preset // container addressed to the channel-active class, whose Comp chunk decodes — through the // instrument's OWN reader — to a ComponentState with THAT id selected, no zones, default // mono. If this fails, the drop would instantiate a blank/wrong instance. static void testPresetRoundTripsThroughInstrumentReader() { const std::string id = "cap-7f3a-guid"; const std::vector preset = buildInstrumentDropPreset(id); CHECK(!preset.empty()); const ParsedPreset p = parsePreset(preset); CHECK(p.ok); CHECK(p.classId == vstClassIdHex()); // The Comp chunk must be EXACTLY the pre-wrap state bytes (no corruption, no framing // bleed into the component stream). CHECK(p.compChunk == instrumentDropStateBytes(id)); const ComponentState cs = deserializeComponentState(p.compChunk, kRate); CHECK(cs.selectionId == id); // the capture IS selected — the whole point // A drop selects one capture and leaves the parameter set at its defaults. CHECK(!cs.params.rootOverride && !cs.params.loopOverride && !cs.params.startPoint); CHECK(cs.channelMode == ChannelMode::Mono); // fresh-instance default CHECK(cs.lastConsumedAssignGeneration == 0); // fresh instance, no consumed assign } // Container layout invariants pinned against the reference writer's constants // (public.sdk vstpresetfile.cpp: kHeaderSize 48, data area first, list last). static void testPresetLayoutInvariants() { const std::string id = "abc"; const std::vector state = instrumentDropStateBytes(id); const std::vector preset = buildInstrumentDropPreset(id); CHECK(fourCCAt(preset, 0, "VST3")); CHECK(readU32LE(preset, 4) == 1); const std::uint64_t listOffset = readU64LE(preset, 40); CHECK(listOffset == 48 + state.size()); // Comp data sits at offset 48 CHECK(fourCCAt(preset, static_cast(listOffset), "List")); CHECK(readU32LE(preset, static_cast(listOffset) + 4) == 1); // one entry CHECK(fourCCAt(preset, static_cast(listOffset) + 8, "Comp")); CHECK(readU64LE(preset, static_cast(listOffset) + 12) == 48); CHECK(readU64LE(preset, static_cast(listOffset) + 20) == state.size()); CHECK(preset.size() == static_cast(listOffset) + 8 + 20); // nothing trails } // A GUID-shaped id with bytes that would trip a naive delimiter-based encoder round-trips // whole (the length-prefixed component-state framing carries arbitrary bytes). static void testGuidLikeIdRoundTrips() { const std::string id = "{9A2F0C11-4B6E-4D01-8F3A-0011223344FF}"; const ParsedPreset p = parsePreset(buildInstrumentDropPreset(id)); CHECK(p.ok); const ComponentState cs = deserializeComponentState(p.compChunk, kRate); CHECK(cs.selectionId == id); } // An empty id yields the empty-state preset: it still parses cleanly to {"", no zones} — the // S10 silent empty state. (The shell guards against dropping nothing; the pure contract holds.) static void testEmptyIdYieldsEmptyState() { const ParsedPreset p = parsePreset(buildInstrumentDropPreset("")); CHECK(p.ok); CHECK(!p.compChunk.empty()); // still a versioned envelope, just an empty selection const ComponentState cs = deserializeComponentState(p.compChunk, kRate); CHECK(cs.selectionId.empty()); CHECK(!cs.params.rootOverride && !cs.params.loopOverride && !cs.params.startPoint); } // Deterministic: the same id always produces the same bytes (no time/random in the path). static void testDeterministic() { CHECK(buildInstrumentDropPreset("abc") == buildInstrumentDropPreset("abc")); CHECK(buildInstrumentDropPreset("abc") != buildInstrumentDropPreset("abd")); } // A malformed class ID (not exactly 32 chars) yields an empty image — contract violation, // never a truncated/garbage preset handed to REAPER. static void testBadClassIdRejected() { const std::vector state = instrumentDropStateBytes("x"); CHECK(buildVstPresetBytes("TOO-SHORT", state).empty()); CHECK(buildVstPresetBytes(std::string(33, 'A'), state).empty()); CHECK(!buildVstPresetBytes(std::string(32, 'A'), state).empty()); } // --- Surface classification --------------------------------------------------- // // THE RULE (prefix-based — see instrument_drop.h): the SDK warns GetThingFromPoint "may append // additional information", so exact-token matching (a previous, DAW-falsified predicate) is // wrong. Ordering is load-bearing: the embed strip is matched BEFORE the track panel, and the // track panel now claims the WHOLE "tcp*"/"mcp*" family rather than just its FX sub-elements. using ui::ReaperSurface; static ReaperSurface surfaceFor(const std::string& info) { return classifyReaperSurface(info); } // The FX chain / floating-FX windows. static void testFxWindowIsFxSurface() { CHECK(surfaceFor("fx_chain") == ReaperSurface::FxSurface); CHECK(surfaceFor("fx_0") == ReaperSurface::FxSurface); CHECK(surfaceFor("fx_12") == ReaperSurface::FxSurface); } // The FX-button family within the TCP/MCP — the surface the glyph-only rule used to be limited // to, still an instrument surface (as TrackPanel, which resolves identically). static void testTcpMcpFxFamilyIsTrackPanel() { CHECK(surfaceFor("tcp.fx") == ReaperSurface::TrackPanel); CHECK(surfaceFor("mcp.fx") == ReaperSurface::TrackPanel); CHECK(surfaceFor("tcp.fxbyp") == ReaperSurface::TrackPanel); CHECK(surfaceFor("tcp.fxparm") == ReaperSurface::TrackPanel); CHECK(surfaceFor("mcp.fxlist") == ReaperSurface::TrackPanel); CHECK(surfaceFor("tcp.fx.1") == ReaperSurface::TrackPanel); // appended info CHECK(surfaceFor("tcp.fx extra") == ReaperSurface::TrackPanel); // appended info, arbitrary form } // THE ROOT-CAUSE FIX: the bare panel token and every non-FX sub-element are now the instrument // hotspot too. A TCP too narrow to draw the FX button reports "tcp", which under the old // glyph-only rule produced a cue-less no-op. static void testWholeTrackPanelIsTheHotspot() { CHECK(surfaceFor("tcp") == ReaperSurface::TrackPanel); // bare track control panel CHECK(surfaceFor("mcp") == ReaperSurface::TrackPanel); // bare mixer control panel CHECK(surfaceFor("tcp.mute") == ReaperSurface::TrackPanel); // mute button CHECK(surfaceFor("tcp.vol") == ReaperSurface::TrackPanel); // volume fader CHECK(surfaceFor("tcp.meter") == ReaperSurface::TrackPanel); // meter CHECK(surfaceFor("tcp.f") == ReaperSurface::TrackPanel); // truncated token — still the panel } // The embed strip is where a ReaSampler 9000 instance already draws inline via // IReaperUIEmbedInterface. It must NOT resolve to a hotspot — a drop there would stack a second // instance on the first. Matched before the "tcp"/"mcp" rule, so widening the panel hotspot // cannot swallow it; do not reorder these two checks in the classifier. static void testEmbedStripIsItsOwnSurface() { CHECK(surfaceFor("tcp.fxembed") == ReaperSurface::FxEmbed); CHECK(surfaceFor("mcp.fxembed") == ReaperSurface::FxEmbed); CHECK(surfaceFor("tcp.fxembed.1") == ReaperSurface::FxEmbed); CHECK(surfaceFor("mcp.fxembed extra") == ReaperSurface::FxEmbed); } // The arrange, including a token with appended information. static void testArrangeIsArrange() { CHECK(surfaceFor("arrange") == ReaperSurface::Arrange); CHECK(surfaceFor("arrange extra") == ReaperSurface::Arrange); } // Anything else REAPER names is Other — a defined refusal, never a guessed outcome. Includes // tokens REAPER may add in future versions. static void testUnnamedReaperSurfacesAreOther() { CHECK(surfaceFor("spacer_0") == ReaperSurface::Other); CHECK(surfaceFor("trans") == ReaperSurface::Other); CHECK(surfaceFor("envcp") == ReaperSurface::Other); CHECK(surfaceFor("ruler") == ReaperSurface::Other); CHECK(surfaceFor("something_reaper_adds_in_2030") == ReaperSurface::Other); } // An empty info string is Other, NOT an off-REAPER verdict. GetThingFromPoint documents no // off-REAPER return at all, so its silence over the transport, the toolbar or the docker chrome // says only "nothing I name" — reading it as "the user left REAPER" is what handed live drags to // OLE mid-gesture. Whether the pointer left REAPER is a window-ownership fact the shell proves // separately (drag_out_win::pointerOverHostWindow) and feeds to the law as // DropContext::pointerOffHost; no classifier output can produce a hand-off on its own. static void testEmptyInfoIsOtherNotOffReaper() { CHECK(surfaceFor("") == ReaperSurface::Other); } // There is deliberately no second empty-info case: track presence stopped being an input to the // classifier when the off-REAPER verdict left the vocabulary. "No classifier output can trigger a // hand-off" is now structural (no such member exists; test_drag_out's static_assert pins the // member count) rather than something a per-token loop could falsify. // Do not reintroduce a per-surface "capture carries" loop test: buildInstrumentDropPreset takes // only sampleId (proven by testPresetRoundTripsThroughInstrumentReader), and per-surface // coverage already exists above — a loop with an identical body per surface string can't // distinguish them. // --- All-or-nothing rollback -------------------------------------------------- // The add failed: nothing was created, so there is nothing to delete and nothing loaded. static void testAddFailureLeavesNothingToRollBack() { const DropOutcome out = decideDropOutcome(DropAttempt{-1, false}); CHECK(!out.loaded); CHECK(out.rollbackFxIndex < 0); } // The instance was created but the preset did not apply: the caller MUST delete that exact // index — an instance without its capture is the orphan the contract forbids. static void testPresetFailureRollsBackTheCreatedIndex() { const DropOutcome zero = decideDropOutcome(DropAttempt{0, false}); CHECK(!zero.loaded); CHECK(zero.rollbackFxIndex == 0); const DropOutcome later = decideDropOutcome(DropAttempt{4, false}); CHECK(!later.loaded); CHECK(later.rollbackFxIndex == 4); // Container-addressed indices (0x2000000-flagged) roll back by the same rule — the // obligation follows the index REAPER handed back, whatever space it names. const DropOutcome inContainer = decideDropOutcome(DropAttempt{0x2000000 + 5, false}); CHECK(!inContainer.loaded); CHECK(inContainer.rollbackFxIndex == 0x2000000 + 5); } // Both halves succeeded: loaded, and nothing to undo. static void testSuccessKeepsTheInstance() { const DropOutcome out = decideDropOutcome(DropAttempt{2, true}); CHECK(out.loaded); CHECK(out.rollbackFxIndex < 0); } // A "preset applied" report with no instance behind it can never read as loaded. static void testNoInstanceIsNeverLoaded() { const DropOutcome out = decideDropOutcome(DropAttempt{-1, true}); CHECK(!out.loaded); CHECK(out.rollbackFxIndex < 0); } int main() { testClassIdHexPinnedPerChannel(); testPresetRoundTripsThroughInstrumentReader(); testPresetLayoutInvariants(); testGuidLikeIdRoundTrips(); testEmptyIdYieldsEmptyState(); testDeterministic(); testBadClassIdRejected(); testFxWindowIsFxSurface(); testTcpMcpFxFamilyIsTrackPanel(); testWholeTrackPanelIsTheHotspot(); testEmbedStripIsItsOwnSurface(); testArrangeIsArrange(); testUnnamedReaperSurfacesAreOther(); testEmptyInfoIsOtherNotOffReaper(); testAddFailureLeavesNothingToRollBack(); testPresetFailureRollsBackTheCreatedIndex(); testSuccessKeepsTheInstance(); testNoInstanceIsNeverLoaded(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }