Merge dev (Phase D1) into p1-w9-bank-panel
# Conflicts: # CMakeLists.txt
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user