Files
reasampler/tests/test_instrument_drop.cpp
T

311 lines
16 KiB
C++

// 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 <cstdio>
#include <cstdint>
#include <string>
#include <vector>
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<std::uint8_t>& b, std::size_t at) {
return static_cast<std::uint32_t>(b[at]) | (static_cast<std::uint32_t>(b[at + 1]) << 8) |
(static_cast<std::uint32_t>(b[at + 2]) << 16) |
(static_cast<std::uint32_t>(b[at + 3]) << 24);
}
static std::uint64_t readU64LE(const std::vector<std::uint8_t>& b, std::size_t at) {
std::uint64_t v = 0;
for (int i = 7; i >= 0; --i) v = (v << 8) | b[at + static_cast<std::size_t>(i)];
return v;
}
static bool fourCCAt(const std::vector<std::uint8_t>& b, std::size_t at, const char* id) {
return b.size() >= at + 4 && b[at] == static_cast<std::uint8_t>(id[0]) &&
b[at + 1] == static_cast<std::uint8_t>(id[1]) &&
b[at + 2] == static_cast<std::uint8_t>(id[2]) &&
b[at + 3] == static_cast<std::uint8_t>(id[3]);
}
struct ParsedPreset {
bool ok = false;
std::string classId;
std::vector<std::uint8_t> compChunk;
};
static ParsedPreset parsePreset(const std::vector<std::uint8_t>& 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<std::size_t>(listOffset), "List")) return p;
const std::uint32_t count = readU32LE(b, static_cast<std::size_t>(listOffset) + 4);
std::size_t at = static_cast<std::size_t>(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<std::ptrdiff_t>(off),
b.begin() + static_cast<std::ptrdiff_t>(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<std::uint8_t> 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
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
}
// 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<std::uint8_t> state = instrumentDropStateBytes(id);
const std::vector<std::uint8_t> 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<std::size_t>(listOffset), "List"));
CHECK(readU32LE(preset, static_cast<std::size_t>(listOffset) + 4) == 1); // one entry
CHECK(fourCCAt(preset, static_cast<std::size_t>(listOffset) + 8, "Comp"));
CHECK(readU64LE(preset, static_cast<std::size_t>(listOffset) + 12) == 48);
CHECK(readU64LE(preset, static_cast<std::size_t>(listOffset) + 20) == state.size());
CHECK(preset.size() == static_cast<std::size_t>(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.map.zones.empty());
}
// 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<std::uint8_t> 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());
}
// --- FX-hotspot classification (S-VIEW-BUG-1 / S-GA-DropFX) -------------------
//
// THE RULE (prefix-based — see instrument_drop.h): "fx_*" names the FX-chain / floating-FX
// windows; "tcp.fx*" / "mcp.fx*" name the TCP/MCP FX button and its sibling FX sub-elements.
// The SDK warns GetThingFromPoint "may append additional information", so exact-token
// matching (the previous, DAW-falsified predicate) is wrong; the prefix family is the
// documented-adjacent surface. Bare "tcp"/"mcp" and non-FX sub-elements are NOT hotspots.
// The TCP/MCP FX-button family arms an instrument drop — including sibling FX sub-elements
// and tokens with appended information.
static void testTcpMcpFxFamilyIsHotspot() {
CHECK(infoNamesFxHotspot("tcp.fx")); // TCP FX button (WALTER element name)
CHECK(infoNamesFxHotspot("mcp.fx")); // MCP FX button
CHECK(infoNamesFxHotspot("tcp.fxbyp")); // FX bypass — sibling FX element
CHECK(infoNamesFxHotspot("tcp.fxparm")); // FX param knob area — sibling FX element
CHECK(infoNamesFxHotspot("mcp.fxlist")); // MCP FX insert list
CHECK(infoNamesFxHotspot("tcp.fx.1")); // appended info (SDK: "may append...")
CHECK(infoNamesFxHotspot("tcp.fx extra")); // appended info, arbitrary form
}
// The FX chain / floating-FX windows (the surfaces the ORIGINAL predicate matched) still
// classify as hotspots — the fix does not regress the "fx_" surface.
static void testFxWindowStillHotspot() {
CHECK(infoNamesFxHotspot("fx_chain")); // FX chain window
CHECK(infoNamesFxHotspot("fx_0")); // first FX, floating
CHECK(infoNamesFxHotspot("fx_12")); // arbitrary floating-FX index
}
// Non-FX surfaces are NOT hotspots — a drop here is not an instrument drop (it would fall
// through to the OS drag / no-op). This includes the bare TCP/MCP tokens and all non-FX
// "tcp.*"/"mcp.*" sub-elements (e.g. mute button, volume fader, track name, meter).
static void testNonFxSurfacesAreNotHotspot() {
CHECK(!infoNamesFxHotspot("tcp")); // bare track control panel — NOT an FX hotspot
CHECK(!infoNamesFxHotspot("mcp")); // bare mixer control panel — NOT an FX hotspot
CHECK(!infoNamesFxHotspot("tcp.mute")); // mute button — track panel, not FX
CHECK(!infoNamesFxHotspot("tcp.vol")); // volume fader — track panel, not FX
CHECK(!infoNamesFxHotspot("tcp.f")); // truncated non-FX token — prefix must be whole
CHECK(!infoNamesFxHotspot("arrange"));
CHECK(!infoNamesFxHotspot("spacer_0"));
CHECK(!infoNamesFxHotspot("")); // pointer over nothing REAPER classifies
CHECK(!infoNamesFxHotspot("trans")); // transport
CHECK(!infoNamesFxHotspot("envcp")); // envelope control panel — a track thing, not FX
}
// The embed-strip sub-element is NOT a hotspot. "tcp.fxembed" / "mcp.fxembed" is the surface
// where a ReaSampler 9000 instance draws inline in the TCP/MCP via IReaperUIEmbedInterface.
// Dropping a card there must NOT add a SECOND instance on top of the existing embed — the
// drop should be ignored (no instrument drop), even though the token starts with "tcp.fx".
// This documents the explicit exclusion in infoNamesFxHotspot and would catch a regression if
// the exclude guard were accidentally removed.
static void testEmbedStripIsNotHotspot() {
CHECK(!infoNamesFxHotspot("tcp.fxembed")); // TCP embed strip — existing instance's surface
CHECK(!infoNamesFxHotspot("mcp.fxembed")); // MCP embed strip — existing instance's surface
// With hypothetically appended info (SDK "may append") — still excluded.
CHECK(!infoNamesFxHotspot("tcp.fxembed.1"));
CHECK(!infoNamesFxHotspot("mcp.fxembed extra"));
}
// --- Drop surface parity: container vs FX button ------------------------------
//
// The container-drop regression (instance loads, capture does not): the two surfaces must be
// one code path carrying one payload. buildInstrumentDropPreset takes only sampleId, so the
// payload side of that claim is already proven once by testPresetRoundTripsThroughInstrumentReader
// and every "fx_"/"tcp.fx"/"mcp.fx" surface classifying as a hotspot is proven by
// testTcpMcpFxFamilyIsHotspot / testFxWindowStillHotspot. A loop that reruns both against a
// fixed sampleId per surface string can't distinguish the surfaces (the loop body is identical
// every iteration) — it isn't a stronger test than those two, so there is no separate test here.
// The one thing that DOES vary by surface — the shell's TrackFX_AddByName `instantiate` value
// picked for a container drop vs. a bare FX-button drop — lives in instrument_drop_win.cpp and
// is untestable without a live DAW (GetThingFromPoint/TrackFX_AddByName have no pure model).
// --- 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();
testTcpMcpFxFamilyIsHotspot();
testFxWindowStillHotspot();
testNonFxSurfacesAreNotHotspot();
testEmbedStripIsNotHotspot();
testAddFailureLeavesNothingToRollBack();
testPresetFailureRollsBackTheCreatedIndex();
testSuccessKeepsTheInstance();
testNoInstanceIsNeverLoaded();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}