Files
reasampler/tests/test_view_mode_model.cpp
T

400 lines
16 KiB
C++

// Standalone tests for reasampler::ViewModeModel — no REAPER, no test framework.
// Mirror of test_bank_model: iterate the hard logic outside the DAW.
//
// Covers (PLAN.md D1 test cases):
// 1. N-mode proven — >=3 modes, membership + derivation still correct.
// 2. Parent derivation — a folder with descendant leaves in different modes is
// visible in each of those modes.
// 3. Restore round-trip — snapshot -> park -> restore returns every driven flag to
// its captured value; includes the "flag already at 0 stays 0" (no default).
// 4. show-both leaf never appears in a park op-list and is visible in all modes.
// 5. Unknown/stale GUID tolerated (ignore-and-prune, no crash).
// 6. JSON round-trip lossless: modes + membership + show-both + snapshots + active.
// 7. planToggle park path: fxOffline is empty (shell-expands-FX contract).
#include "../src/view_mode_model.h"
#include <algorithm>
#include <cstdio>
#include <string>
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)
// -- helpers -----------------------------------------------------------------
static bool visibleHas(const std::set<std::string>& v, const std::string& g) {
return v.count(g) > 0;
}
// Does any park TrackPlan in the plan target `guid`?
static bool parkTargets(const TogglePlan& plan, const std::string& guid) {
for (const auto& p : plan.park)
for (const auto& f : p.flags)
if (f.guid == guid) return true;
return false;
}
// Find the single restore plan for `guid`, or nullptr.
static const TrackPlan* restoreFor(const TogglePlan& plan, const std::string& guid) {
for (const auto& p : plan.restore)
if (!p.flags.empty() && p.flags.front().guid == guid) return &p;
return nullptr;
}
static int flagValue(const TrackPlan& p, Flag f) {
for (const auto& op : p.flags)
if (op.flag == f) return op.value;
return -999; // sentinel: flag absent
}
// -- 1. N-mode proven --------------------------------------------------------
static void testNModeRegistryAndMembership() {
ViewModeModel vm;
// Seeded: Arrange + Design.
CHECK(vm.modes().size() == 2);
CHECK(vm.modes().contains(kArrangeModeId));
CHECK(vm.modes().contains(kDesignModeId));
CHECK(vm.activeModeId() == kArrangeModeId);
// Add a third mode — proves N-mode, not boolean.
CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));
CHECK(vm.modes().size() == 3);
CHECK(vm.modes().contains("mixdown"));
// Duplicate id and empty id are rejected without mutation.
CHECK(!vm.modes().add(Mode{"mixdown", "Dup", 5}));
CHECK(!vm.modes().add(Mode{"", "Empty", 6}));
CHECK(vm.modes().size() == 3);
// Membership across three modes.
CHECK(vm.membership().tag("{A}", kArrangeModeId));
CHECK(vm.membership().tag("{D}", kDesignModeId));
CHECK(vm.membership().tag("{M}", "mixdown"));
// Leaf-belongs rule holds in each mode.
CHECK(vm.leafBelongsToMode("{D}", kDesignModeId));
CHECK(!vm.leafBelongsToMode("{D}", kArrangeModeId));
CHECK(vm.leafBelongsToMode("{M}", "mixdown"));
CHECK(!vm.leafBelongsToMode("{M}", kDesignModeId));
// Untagged leaf defaults to Arrange, and only Arrange.
CHECK(vm.leafBelongsToMode("{UNTAGGED}", kArrangeModeId));
CHECK(!vm.leafBelongsToMode("{UNTAGGED}", kDesignModeId));
// Retag moves the leaf (single-mode semantics).
CHECK(vm.membership().tag("{D}", "mixdown"));
CHECK(vm.leafBelongsToMode("{D}", "mixdown"));
CHECK(!vm.leafBelongsToMode("{D}", kDesignModeId));
// Untag returns to the Arrange default.
CHECK(vm.membership().untag("{D}"));
CHECK(vm.leafBelongsToMode("{D}", kArrangeModeId));
CHECK(!vm.membership().untag("{D}")); // second untag is a no-op
// setActiveMode rejects an unregistered id, accepts a registered one.
CHECK(!vm.setActiveMode("nope"));
CHECK(vm.activeModeId() == kArrangeModeId);
CHECK(vm.setActiveMode("mixdown"));
CHECK(vm.activeModeId() == "mixdown");
}
// -- 2. Parent derivation ----------------------------------------------------
static void testParentDerivationMultiMode() {
ViewModeModel vm;
// Folder {F} holds two leaves: {L1} in Arrange (untagged default), {L2} in Design.
FolderTree tree;
tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
tree.nodes.push_back(FolderNode{"{L1}", "{F}", false});
tree.nodes.push_back(FolderNode{"{L2}", "{F}", false});
vm.membership().tag("{L2}", kDesignModeId);
// {L1} stays untagged ⇒ Arrange.
auto arrange = vm.visibleTracks(tree, kArrangeModeId);
auto design = vm.visibleTracks(tree, kDesignModeId);
// The parent is visible in BOTH modes because it has a descendant in each.
CHECK(visibleHas(arrange, "{F}"));
CHECK(visibleHas(design, "{F}"));
// Leaves appear only in their own mode.
CHECK(visibleHas(arrange, "{L1}") && !visibleHas(arrange, "{L2}"));
CHECK(visibleHas(design, "{L2}") && !visibleHas(design, "{L1}"));
// Nested folder chain: grandparent {G} > parent {F2} > leaf {L3} (Design).
// The whole chain up to the root must be visible in Design.
FolderTree nested;
nested.nodes.push_back(FolderNode{"{G}", "", true});
nested.nodes.push_back(FolderNode{"{F2}", "{G}", true});
nested.nodes.push_back(FolderNode{"{L3}", "{F2}", false});
ViewModeModel vm2;
vm2.membership().tag("{L3}", kDesignModeId);
auto d2 = vm2.visibleTracks(nested, kDesignModeId);
CHECK(visibleHas(d2, "{L3}"));
CHECK(visibleHas(d2, "{F2}"));
CHECK(visibleHas(d2, "{G}"));
// In Arrange, none of the chain is visible (no Arrange leaf under it).
auto a2 = vm2.visibleTracks(nested, kArrangeModeId);
CHECK(!visibleHas(a2, "{L3}") && !visibleHas(a2, "{F2}") && !visibleHas(a2, "{G}"));
// A parent is NEVER parked, in either mode.
auto planD = vm.planToggle(tree, kDesignModeId);
auto planA = vm.planToggle(tree, kArrangeModeId);
CHECK(!parkTargets(planD, "{F}"));
CHECK(!parkTargets(planA, "{F}"));
}
// -- 3. Restore round-trip (the trust anchor) --------------------------------
static void testRestoreRoundTripSnapshotValues() {
// Direct planner check: park is fixed zeros; restore is snapshot verbatim.
TrackSnapshot snap;
snap.showInTcp = 1;
snap.showInMixer = 1;
snap.mainSend = 0; // user had it OUT of the mix for their own reason
snap.fxEnable = 1;
snap.fxOffline = {0, 1, 0}; // slot 1 was already offline before parking
TrackPlan park = makeParkPlan("{T}", /*fxCount=*/3);
CHECK(flagValue(park, Flag::ShowInTcp) == 0);
CHECK(flagValue(park, Flag::ShowInMixer) == 0);
CHECK(flagValue(park, Flag::MainSend) == 0);
CHECK(flagValue(park, Flag::FxEnable) == 0);
CHECK(park.fxOffline.size() == 3);
for (const auto& op : park.fxOffline) CHECK(op.offline == true);
TrackPlan restore = makeRestorePlan("{T}", snap);
// Every flag returns to its CAPTURED value — not a hardcoded "on".
CHECK(flagValue(restore, Flag::ShowInTcp) == 1);
CHECK(flagValue(restore, Flag::ShowInMixer) == 1);
CHECK(flagValue(restore, Flag::MainSend) == 0); // the "already at 0 stays 0" case
CHECK(flagValue(restore, Flag::FxEnable) == 1);
CHECK(restore.fxOffline.size() == 3);
CHECK(restore.fxOffline[0].offline == false);
CHECK(restore.fxOffline[1].offline == true); // was offline pre-park ⇒ stays offline
CHECK(restore.fxOffline[2].offline == false);
// A snapshot entirely at 0 must restore entirely to 0 (no default leaks in).
TrackSnapshot zero; // all zeros, empty fxOffline
TrackPlan rz = makeRestorePlan("{Z}", zero);
CHECK(flagValue(rz, Flag::ShowInTcp) == 0);
CHECK(flagValue(rz, Flag::ShowInMixer) == 0);
CHECK(flagValue(rz, Flag::MainSend) == 0);
CHECK(flagValue(rz, Flag::FxEnable) == 0);
CHECK(rz.fxOffline.empty());
// End-to-end via planToggle: a leaf tagged Design, snapshotted, parked while in
// Arrange, then restored when we toggle back to Design.
ViewModeModel vm;
FolderTree tree;
tree.nodes.push_back(FolderNode{"{DES}", "", false});
vm.membership().tag("{DES}", kDesignModeId);
vm.storeSnapshot("{DES}", snap);
// Toggle to Arrange: {DES} is inactive ⇒ parked.
auto toArrange = vm.planToggle(tree, kArrangeModeId);
CHECK(parkTargets(toArrange, "{DES}"));
CHECK(restoreFor(toArrange, "{DES}") == nullptr); // not restored while inactive
// Toggle to Design: {DES} is active AND has a snapshot ⇒ restored from it.
auto toDesign = vm.planToggle(tree, kDesignModeId);
CHECK(!parkTargets(toDesign, "{DES}"));
const TrackPlan* r = restoreFor(toDesign, "{DES}");
CHECK(r != nullptr);
if (r) {
CHECK(flagValue(*r, Flag::MainSend) == 0); // captured 0 comes back 0
CHECK(flagValue(*r, Flag::ShowInTcp) == 1);
}
}
// -- 4. show-both leaf --------------------------------------------------------
static void testShowBothNeverParkedVisibleEverywhere() {
ViewModeModel vm;
CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));
FolderTree tree;
tree.nodes.push_back(FolderNode{"{SB}", "", false});
// Tag into Design, then pin show-both.
vm.membership().tag("{SB}", kDesignModeId);
CHECK(vm.membership().setShowBoth("{SB}", true));
CHECK(vm.membership().isShowBoth("{SB}"));
// Visible in EVERY mode.
CHECK(visibleHas(vm.visibleTracks(tree, kArrangeModeId), "{SB}"));
CHECK(visibleHas(vm.visibleTracks(tree, kDesignModeId), "{SB}"));
CHECK(visibleHas(vm.visibleTracks(tree, "mixdown"), "{SB}"));
// Never parked, in any mode — even a mode it isn't tagged into.
CHECK(!parkTargets(vm.planToggle(tree, kArrangeModeId), "{SB}"));
CHECK(!parkTargets(vm.planToggle(tree, kDesignModeId), "{SB}"));
CHECK(!parkTargets(vm.planToggle(tree, "mixdown"), "{SB}"));
// Clearing show-both restores normal one-mode parking: now in Arrange it parks.
CHECK(vm.membership().setShowBoth("{SB}", false));
CHECK(parkTargets(vm.planToggle(tree, kArrangeModeId), "{SB}"));
CHECK(!parkTargets(vm.planToggle(tree, kDesignModeId), "{SB}"));
}
// -- 5. Unknown/stale GUID tolerated -----------------------------------------
static void testStaleGuidTolerated() {
ViewModeModel vm;
// Tag two leaves, but the tree only knows one — the other GUID is stale (its
// track was deleted / restructured while parked).
vm.membership().tag("{LIVE}", kDesignModeId);
vm.membership().tag("{GHOST}", kDesignModeId);
vm.storeSnapshot("{GHOST}", TrackSnapshot{}); // stale snapshot too
FolderTree tree;
tree.nodes.push_back(FolderNode{"{LIVE}", "", false});
// {GHOST} absent from the tree.
// No crash; the stale GUID is simply ignored (prune-safe).
auto plan = vm.planToggle(tree, kArrangeModeId);
CHECK(parkTargets(plan, "{LIVE}")); // live leaf still planned
CHECK(!parkTargets(plan, "{GHOST}")); // stale leaf never emitted
// Visibility derivation also ignores the stale GUID without incident.
auto vis = vm.visibleTracks(tree, kDesignModeId);
CHECK(visibleHas(vis, "{LIVE}"));
CHECK(!visibleHas(vis, "{GHOST}"));
// An empty tree with tagged members: nothing planned, no crash.
FolderTree empty;
auto emptyPlan = vm.planToggle(empty, kDesignModeId);
CHECK(emptyPlan.park.empty() && emptyPlan.restore.empty());
}
// -- 6. JSON round-trip lossless ---------------------------------------------
static void testJsonRoundTrip() {
ViewModeModel vm;
// Modes: seeded pair + a third; also an out-of-order ordinal to prove sorting
// survives round-trip.
CHECK(vm.modes().add(Mode{"print", "Print \"stem\"\n", 5}));
CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));
// Membership: a plain Design leaf, a show-both leaf, an Arrange leaf, and a
// leaf carrying multiple modes (representable via restore; exercises the set).
vm.membership().tag("{A}", kArrangeModeId);
vm.membership().tag("{D}", kDesignModeId);
vm.membership().tag("{SB}", "mixdown");
vm.membership().setShowBoth("{SB}", true);
Membership multi;
multi.modeIds = {kDesignModeId, "mixdown"};
multi.showBoth = false;
CHECK(vm.membership().restore("{MULTI}", multi));
// Snapshots: one full, one with a per-FX vector, including the tricky 0-values.
TrackSnapshot s1; s1.showInTcp = 1; s1.showInMixer = 0; s1.mainSend = 1;
s1.fxEnable = 0; s1.fxOffline = {1, 0, 1, 1};
vm.storeSnapshot("{D}", s1);
TrackSnapshot s2; // all zeros, empty fx vector
vm.storeSnapshot("{A}", s2);
// Active mode set to a non-default.
CHECK(vm.setActiveMode("mixdown"));
std::string json = vm.serialize();
auto back = ViewModeModel::deserialize(json);
CHECK(back.has_value());
CHECK(back && *back == vm);
// String form is stable across a second round-trip.
if (back) CHECK(back->serialize() == json);
// Spot-check the load-bearing bits survived.
if (back) {
CHECK(back->activeModeId() == "mixdown");
CHECK(back->modes().size() == 4);
const Mode* print = back->modes().query("print");
CHECK(print && print->displayName == "Print \"stem\"\n" && print->ordinal == 5);
CHECK(back->membership().isShowBoth("{SB}"));
const Membership* mm = back->membership().query("{MULTI}");
CHECK(mm && mm->modeIds.size() == 2 && mm->modeIds.count("mixdown"));
const TrackSnapshot* snap = back->snapshot("{D}");
CHECK(snap && snap->mainSend == 1 && snap->fxEnable == 0);
CHECK(snap && snap->fxOffline.size() == 4 && snap->fxOffline[1] == 0);
}
}
static void testEmptyModelRoundTrip() {
ViewModeModel vm; // default: Arrange + Design seeded, active = Arrange, no members
std::string json = vm.serialize();
auto back = ViewModeModel::deserialize(json);
CHECK(back.has_value());
CHECK(back && *back == vm);
// Lenient empty root ⇒ a default-seeded model.
auto empty = ViewModeModel::deserialize("{}");
CHECK(empty.has_value());
CHECK(empty && empty->modes().size() == 2);
CHECK(empty && empty->activeModeId() == kArrangeModeId);
CHECK(empty && empty->membership().empty());
}
static void testMalformedJson() {
const char* bad[] = {
"",
"{",
"not json",
"{\"modes\":[",
"{\"modes\":[{\"id\":\"x\"", // truncated mode
"{\"activeMode\":\"ghost\"}", // active mode not registered
"{\"modes\":[{\"id\":\"a\",\"ordinal\":0},{\"id\":\"a\",\"ordinal\":1}]}", // dup id
"{\"membership\":[{\"guid\":\"\"}]}", // empty guid
"{\"snapshots\":[{\"showInTcp\":1}]}", // snapshot without guid
"{\"snapshots\":[{\"guid\":\"x\",\"fxOffline\":[1,notanumber]}]}",
"{\"modes\":[]}trailing", // trailing garbage
};
for (const char* j : bad) {
auto r = ViewModeModel::deserialize(j);
CHECK(!r.has_value());
}
}
// -- 7. planToggle park path: fxOffline is empty (shell-expands-FX contract) --
//
// planToggle calls makeParkPlan(guid, /*fxCount=*/0) for each inactive leaf.
// The D2 shell is responsible for expanding per-FX offline ops using
// TrackFX_GetCount — the pure model has no access to REAPER FX counts at plan
// time. This test pins that contract so a regression that passes a non-zero
// count (and emits FX ops prematurely) is caught immediately.
// The direct makeParkPlan(guid, 3) path (non-zero fxCount) is covered by
// testRestoreRoundTripSnapshotValues above.
static void testPlanToggleParkHasEmptyFxOffline() {
ViewModeModel vm;
FolderTree tree;
tree.nodes.push_back(FolderNode{"{LEAF}", "", false});
vm.membership().tag("{LEAF}", kDesignModeId);
// Toggle to Arrange: {LEAF} is inactive ⇒ park plan emitted.
auto plan = vm.planToggle(tree, kArrangeModeId);
CHECK(plan.park.size() == 1);
// The park plan must have an empty fxOffline — the shell expands FX ops.
CHECK(plan.park[0].fxOffline.empty());
// Scalar flags must still be present (the four park zeros).
CHECK(plan.park[0].flags.size() == 4);
}
int main() {
testNModeRegistryAndMembership();
testParentDerivationMultiMode();
testRestoreRoundTripSnapshotValues();
testShowBothNeverParkedVisibleEverywhere();
testStaleGuidTolerated();
testJsonRoundTrip();
testEmptyModelRoundTrip();
testMalformedJson();
testPlanToggleParkHasEmptyFxOffline();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}