rename view_model -> view_mode_model; add shell-expands-FX test and parse-site comments

This commit is contained in:
2026-07-22 20:37:17 -04:00
parent 33ffb45a45
commit 25cfb62d46
4 changed files with 93 additions and 49 deletions
+7 -7
View File
@@ -37,13 +37,13 @@ add_library(capture_paths STATIC src/capture_paths.cpp)
target_include_directories(capture_paths PUBLIC src) target_include_directories(capture_paths PUBLIC src)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 2c) Pure view_model library — NO REAPER, NO SWELL. The Design View heart # 2c) Pure view_mode_model library — NO REAPER, NO SWELL. The Design View heart
# (Phase D1): mode registry + GUID-keyed membership index + folder-tree-aware # (Phase D1): mode registry + GUID-keyed membership index + folder-tree-aware
# visibility derivation + parking/restore planner + JSON round-trip. Mirror of # visibility derivation + parking/restore planner + JSON round-trip. Mirror of
# bank_model; the folder tree is an INPUT supplied by the D2 shell. # bank_model; the folder tree is an INPUT supplied by the D2 shell.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
add_library(view_model STATIC src/view_model.cpp) add_library(view_mode_model STATIC src/view_mode_model.cpp)
target_include_directories(view_model PUBLIC src) target_include_directories(view_mode_model PUBLIC src)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 3) Standalone tests for the pure modules (run without launching REAPER). # 3) Standalone tests for the pure modules (run without launching REAPER).
@@ -61,9 +61,9 @@ add_executable(capture_paths_tests tests/test_capture_paths.cpp)
target_link_libraries(capture_paths_tests PRIVATE capture_paths) target_link_libraries(capture_paths_tests PRIVATE capture_paths)
add_test(NAME capture_paths_tests COMMAND capture_paths_tests) add_test(NAME capture_paths_tests COMMAND capture_paths_tests)
add_executable(view_model_tests tests/test_view_model.cpp) add_executable(view_mode_model_tests tests/test_view_mode_model.cpp)
target_link_libraries(view_model_tests PRIVATE view_model) target_link_libraries(view_mode_model_tests PRIVATE view_mode_model)
add_test(NAME view_model_tests COMMAND view_model_tests) add_test(NAME view_mode_model_tests COMMAND view_mode_model_tests)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked). # 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
@@ -72,7 +72,7 @@ add_library(reaper_reasampler MODULE
src/main.cpp src/main.cpp
src/capture.cpp src/capture.cpp
src/persist.cpp src/persist.cpp
src/view_model.cpp src/view_mode_model.cpp
) )
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths) target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths)
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC}) target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
+35 -22
View File
@@ -1,4 +1,4 @@
#include "view_model.h" #include "view_mode_model.h"
#include <algorithm> #include <algorithm>
#include <cerrno> #include <cerrno>
@@ -6,7 +6,7 @@
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
// view_model implementation. // view_mode_model implementation.
// //
// JSON is hand-rolled and self-contained, mirroring bank_model's approach (brief: // JSON is hand-rolled and self-contained, mirroring bank_model's approach (brief:
// keep the pure core dependency-free — no third-party JSON lib). A compact writer // keep the pure core dependency-free — no third-party JSON lib). A compact writer
@@ -124,31 +124,31 @@ TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap) {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// ViewModel // ViewModeModel
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
ViewModel::ViewModel() : activeModeId_(kArrangeModeId) {} ViewModeModel::ViewModeModel() : activeModeId_(kArrangeModeId) {}
bool ViewModel::setActiveMode(const std::string& modeId) { bool ViewModeModel::setActiveMode(const std::string& modeId) {
if (!modes_.contains(modeId)) return false; if (!modes_.contains(modeId)) return false;
activeModeId_ = modeId; activeModeId_ = modeId;
return true; return true;
} }
void ViewModel::storeSnapshot(const std::string& guid, const TrackSnapshot& snap) { void ViewModeModel::storeSnapshot(const std::string& guid, const TrackSnapshot& snap) {
snapshots_[guid] = snap; snapshots_[guid] = snap;
} }
void ViewModel::clearSnapshot(const std::string& guid) { void ViewModeModel::clearSnapshot(const std::string& guid) {
snapshots_.erase(guid); snapshots_.erase(guid);
} }
const TrackSnapshot* ViewModel::snapshot(const std::string& guid) const { const TrackSnapshot* ViewModeModel::snapshot(const std::string& guid) const {
auto it = snapshots_.find(guid); auto it = snapshots_.find(guid);
return it == snapshots_.end() ? nullptr : &it->second; return it == snapshots_.end() ? nullptr : &it->second;
} }
bool ViewModel::leafBelongsToMode(const std::string& guid, const std::string& modeId) const { bool ViewModeModel::leafBelongsToMode(const std::string& guid, const std::string& modeId) const {
const Membership* m = membership_.query(guid); const Membership* m = membership_.query(guid);
if (!m) return modeId == kArrangeModeId; // untagged ⇒ Arrange default if (!m) return modeId == kArrangeModeId; // untagged ⇒ Arrange default
if (m->showBoth) return true; // show-both ⇒ every mode if (m->showBoth) return true; // show-both ⇒ every mode
@@ -156,7 +156,7 @@ bool ViewModel::leafBelongsToMode(const std::string& guid, const std::string& mo
return m->modeIds.count(modeId) > 0; return m->modeIds.count(modeId) > 0;
} }
std::set<std::string> ViewModel::visibleTracks(const FolderTree& tree, std::set<std::string> ViewModeModel::visibleTracks(const FolderTree& tree,
const std::string& modeId) const { const std::string& modeId) const {
std::set<std::string> visible; std::set<std::string> visible;
@@ -189,7 +189,7 @@ std::set<std::string> ViewModel::visibleTracks(const FolderTree& tree,
return visible; return visible;
} }
TogglePlan ViewModel::planToggle(const FolderTree& tree, const std::string& targetMode) const { TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const {
TogglePlan plan; TogglePlan plan;
// Index the tree so we can classify each tagged GUID (leaf vs parent vs stale). // Index the tree so we can classify each tagged GUID (leaf vs parent vs stale).
@@ -209,10 +209,10 @@ TogglePlan ViewModel::planToggle(const FolderTree& tree, const std::string& targ
if (const TrackSnapshot* snap = snapshot(guid)) if (const TrackSnapshot* snap = snapshot(guid))
plan.restore.push_back(makeRestorePlan(guid, *snap)); plan.restore.push_back(makeRestorePlan(guid, *snap));
} else { } else {
// Inactive tagged leaf ⇒ park. FX count is unknown to the pure model; // Inactive tagged leaf ⇒ park. fxOffline is intentionally empty here:
// the shell expands per-FX offline from TrackFX_GetCount. We emit the // the D2 shell expands per-FX offline writes using TrackFX_GetCount.
// scalar flags and leave fxOffline to the shell for the count (park // The pure model has no access to REAPER FX counts at plan time;
// offlines ALL, so no per-slot value decision is needed here). // makeParkPlan(guid, 0) emits only the scalar flags as a result.
plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0)); plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0));
} }
} }
@@ -220,7 +220,7 @@ TogglePlan ViewModel::planToggle(const FolderTree& tree, const std::string& targ
return plan; return plan;
} }
bool ViewModel::operator==(const ViewModel& o) const { bool ViewModeModel::operator==(const ViewModeModel& o) const {
return modes_ == o.modes_ && membership_ == o.membership_ && return modes_ == o.modes_ && membership_ == o.membership_ &&
activeModeId_ == o.activeModeId_ && snapshots_ == o.snapshots_; activeModeId_ == o.activeModeId_ && snapshots_ == o.snapshots_;
} }
@@ -301,7 +301,7 @@ private:
} // namespace } // namespace
std::string ViewModel::serialize() const { std::string ViewModeModel::serialize() const {
std::string out; std::string out;
{ {
ObjWriter root(out); ObjWriter root(out);
@@ -378,7 +378,7 @@ namespace {
class Parser { class Parser {
public: public:
explicit Parser(const std::string& s) : s_(s) {} explicit Parser(const std::string& s) : s_(s) {}
bool parseModel(ViewModel& out); bool parseModel(ViewModeModel& out);
private: private:
const std::string& s_; const std::string& s_;
@@ -622,6 +622,15 @@ bool Parser::parseMembership(MembershipIndex& idx) {
// Install the entry verbatim (tag() would clear a multi-mode set and drop // Install the entry verbatim (tag() would clear a multi-mode set and drop
// show-both). A serialized entry is trusted to already satisfy the model's // show-both). A serialized entry is trusted to already satisfy the model's
// invariants. // invariants.
//
// Deliberate tolerance: we do NOT validate that membership modeIds reference
// registered modes, and we do not validate snapshot GUIDs against the index.
// Stale-GUID and stale-mode tolerance is a stated invariant of this model —
// a deserialized entry is treated as trusted data, not as live cross-checked
// state. Rejecting stale entries here would violate that invariant. The one
// exception is activeMode (validated below in parseModel): a persisted active
// mode that no longer exists has an immediate behavioral consequence, so it
// is caught and the parse is rejected.
if (!idx.restore(guid, mem)) return false; if (!idx.restore(guid, mem)) return false;
} while (consume(',')); } while (consume(','));
return consume(']'); return consume(']');
@@ -654,7 +663,7 @@ bool Parser::parseSnapshots(std::map<std::string, TrackSnapshot>& snaps) {
return consume(']'); return consume(']');
} }
bool Parser::parseModel(ViewModel& out) { bool Parser::parseModel(ViewModeModel& out) {
if (!consume('{')) return false; if (!consume('{')) return false;
skipWs(); skipWs();
if (consume('}')) return true; // lenient empty root ⇒ default-seeded model if (consume('}')) return true; // lenient empty root ⇒ default-seeded model
@@ -682,7 +691,11 @@ bool Parser::parseModel(ViewModel& out) {
} else if (key == "snapshots") { } else if (key == "snapshots") {
if (!parseSnapshots(snaps)) return false; if (!parseSnapshots(snaps)) return false;
} else { } else {
if (!skipValue()) return false; // version, unknown keys // Unknown keys and the "version" field are skipped here.
// "version" is serialized as a forward-compat placeholder — there is no
// active version gate yet; all persisted data is parsed the same way
// regardless of the value. A future gate would add a version branch here.
if (!skipValue()) return false;
} }
} while (consume(',')); } while (consume(','));
@@ -701,8 +714,8 @@ bool Parser::parseModel(ViewModel& out) {
} // namespace } // namespace
std::optional<ViewModel> ViewModel::deserialize(const std::string& json) { std::optional<ViewModeModel> ViewModeModel::deserialize(const std::string& json) {
ViewModel vm; ViewModeModel vm;
Parser p(json); Parser p(json);
if (!p.parseModel(vm)) return std::nullopt; if (!p.parseModel(vm)) return std::nullopt;
return vm; return vm;
+10 -6
View File
@@ -1,5 +1,5 @@
#pragma once #pragma once
// view_model — the pure core of the Design View feature, deliberately free of any // view_mode_model — the pure core of the Design View feature, deliberately free of any
// REAPER type so it compiles and unit-tests OUTSIDE the DAW. It is the mirror of // REAPER type so it compiles and unit-tests OUTSIDE the DAW. It is the mirror of
// bank_model: it owns the mode registry, the GUID-keyed membership index, the // bank_model: it owns the mode registry, the GUID-keyed membership index, the
// folder-tree-aware visibility derivation, the parking/restore planner, and the // folder-tree-aware visibility derivation, the parking/restore planner, and the
@@ -239,15 +239,15 @@ struct TogglePlan {
std::vector<TrackPlan> restore; // active leaves returning -> snapshot values std::vector<TrackPlan> restore; // active leaves returning -> snapshot values
}; };
// -- The view model ---------------------------------------------------------- // -- The view mode model -----------------------------------------------------
// //
// Owns the mode registry, the membership index, the active mode, and the durable // Owns the mode registry, the membership index, the active mode, and the durable
// per-track snapshots (kept for tracks currently parked so a save-while-parked // per-track snapshots (kept for tracks currently parked so a save-while-parked
// project restores correctly). Visibility and the toggle plan are computed against // project restores correctly). Visibility and the toggle plan are computed against
// a supplied FolderTree — the tree is never stored. // a supplied FolderTree — the tree is never stored.
class ViewModel { class ViewModeModel {
public: public:
ViewModel(); // Arrange + Design seeded; active mode = Arrange ViewModeModel(); // Arrange + Design seeded; active mode = Arrange
ModeRegistry& modes() { return modes_; } ModeRegistry& modes() { return modes_; }
const ModeRegistry& modes() const { return modes_; } const ModeRegistry& modes() const { return modes_; }
@@ -283,15 +283,19 @@ public:
// leaves that become active AND have a stored snapshot are restored from it. // leaves that become active AND have a stored snapshot are restored from it.
// Parents, show-both leaves, the master, and untagged tracks are never parked. // Parents, show-both leaves, the master, and untagged tracks are never parked.
// Unknown/stale membership GUIDs absent from the tree are ignored (prune-safe). // Unknown/stale membership GUIDs absent from the tree are ignored (prune-safe).
//
// Note: park plans emitted here have an empty fxOffline vector. The D2 shell
// expands per-FX offline writes using TrackFX_GetCount — the pure model has no
// access to REAPER FX counts at plan time.
TogglePlan planToggle(const FolderTree& tree, const std::string& targetMode) const; TogglePlan planToggle(const FolderTree& tree, const std::string& targetMode) const;
bool operator==(const ViewModel& o) const; bool operator==(const ViewModeModel& o) const;
std::string serialize() const; std::string serialize() const;
// Parses a JSON string produced by serialize(). std::nullopt on malformed // Parses a JSON string produced by serialize(). std::nullopt on malformed
// input. On success deserialize(serialize(x)) == x. // input. On success deserialize(serialize(x)) == x.
static std::optional<ViewModel> deserialize(const std::string& json); static std::optional<ViewModeModel> deserialize(const std::string& json);
private: private:
ModeRegistry modes_; ModeRegistry modes_;
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::view_model — no REAPER, no test framework. // Standalone tests for reasampler::ViewModeModel — no REAPER, no test framework.
// Mirror of test_bank_model: iterate the hard logic outside the DAW. // Mirror of test_bank_model: iterate the hard logic outside the DAW.
// //
// Covers (PLAN.md D1 test cases): // Covers (PLAN.md D1 test cases):
@@ -10,8 +10,9 @@
// 4. show-both leaf never appears in a park op-list and is visible in all modes. // 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). // 5. Unknown/stale GUID tolerated (ignore-and-prune, no crash).
// 6. JSON round-trip lossless: modes + membership + show-both + snapshots + active. // 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_model.h" #include "../src/view_mode_model.h"
#include <algorithm> #include <algorithm>
#include <cstdio> #include <cstdio>
@@ -53,7 +54,7 @@ static int flagValue(const TrackPlan& p, Flag f) {
// -- 1. N-mode proven -------------------------------------------------------- // -- 1. N-mode proven --------------------------------------------------------
static void testNModeRegistryAndMembership() { static void testNModeRegistryAndMembership() {
ViewModel vm; ViewModeModel vm;
// Seeded: Arrange + Design. // Seeded: Arrange + Design.
CHECK(vm.modes().size() == 2); CHECK(vm.modes().size() == 2);
CHECK(vm.modes().contains(kArrangeModeId)); CHECK(vm.modes().contains(kArrangeModeId));
@@ -105,7 +106,7 @@ static void testNModeRegistryAndMembership() {
// -- 2. Parent derivation ---------------------------------------------------- // -- 2. Parent derivation ----------------------------------------------------
static void testParentDerivationMultiMode() { static void testParentDerivationMultiMode() {
ViewModel vm; ViewModeModel vm;
// Folder {F} holds two leaves: {L1} in Arrange (untagged default), {L2} in Design. // Folder {F} holds two leaves: {L1} in Arrange (untagged default), {L2} in Design.
FolderTree tree; FolderTree tree;
tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true}); tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
@@ -131,7 +132,7 @@ static void testParentDerivationMultiMode() {
nested.nodes.push_back(FolderNode{"{G}", "", true}); nested.nodes.push_back(FolderNode{"{G}", "", true});
nested.nodes.push_back(FolderNode{"{F2}", "{G}", true}); nested.nodes.push_back(FolderNode{"{F2}", "{G}", true});
nested.nodes.push_back(FolderNode{"{L3}", "{F2}", false}); nested.nodes.push_back(FolderNode{"{L3}", "{F2}", false});
ViewModel vm2; ViewModeModel vm2;
vm2.membership().tag("{L3}", kDesignModeId); vm2.membership().tag("{L3}", kDesignModeId);
auto d2 = vm2.visibleTracks(nested, kDesignModeId); auto d2 = vm2.visibleTracks(nested, kDesignModeId);
CHECK(visibleHas(d2, "{L3}")); CHECK(visibleHas(d2, "{L3}"));
@@ -189,7 +190,7 @@ static void testRestoreRoundTripSnapshotValues() {
// End-to-end via planToggle: a leaf tagged Design, snapshotted, parked while in // End-to-end via planToggle: a leaf tagged Design, snapshotted, parked while in
// Arrange, then restored when we toggle back to Design. // Arrange, then restored when we toggle back to Design.
ViewModel vm; ViewModeModel vm;
FolderTree tree; FolderTree tree;
tree.nodes.push_back(FolderNode{"{DES}", "", false}); tree.nodes.push_back(FolderNode{"{DES}", "", false});
vm.membership().tag("{DES}", kDesignModeId); vm.membership().tag("{DES}", kDesignModeId);
@@ -214,7 +215,7 @@ static void testRestoreRoundTripSnapshotValues() {
// -- 4. show-both leaf -------------------------------------------------------- // -- 4. show-both leaf --------------------------------------------------------
static void testShowBothNeverParkedVisibleEverywhere() { static void testShowBothNeverParkedVisibleEverywhere() {
ViewModel vm; ViewModeModel vm;
CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2})); CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));
FolderTree tree; FolderTree tree;
@@ -243,7 +244,7 @@ static void testShowBothNeverParkedVisibleEverywhere() {
// -- 5. Unknown/stale GUID tolerated ----------------------------------------- // -- 5. Unknown/stale GUID tolerated -----------------------------------------
static void testStaleGuidTolerated() { static void testStaleGuidTolerated() {
ViewModel vm; ViewModeModel vm;
// Tag two leaves, but the tree only knows one — the other GUID is stale (its // Tag two leaves, but the tree only knows one — the other GUID is stale (its
// track was deleted / restructured while parked). // track was deleted / restructured while parked).
vm.membership().tag("{LIVE}", kDesignModeId); vm.membership().tag("{LIVE}", kDesignModeId);
@@ -273,7 +274,7 @@ static void testStaleGuidTolerated() {
// -- 6. JSON round-trip lossless --------------------------------------------- // -- 6. JSON round-trip lossless ---------------------------------------------
static void testJsonRoundTrip() { static void testJsonRoundTrip() {
ViewModel vm; ViewModeModel vm;
// Modes: seeded pair + a third; also an out-of-order ordinal to prove sorting // Modes: seeded pair + a third; also an out-of-order ordinal to prove sorting
// survives round-trip. // survives round-trip.
CHECK(vm.modes().add(Mode{"print", "Print \"stem\"\n", 5})); CHECK(vm.modes().add(Mode{"print", "Print \"stem\"\n", 5}));
@@ -301,7 +302,7 @@ static void testJsonRoundTrip() {
CHECK(vm.setActiveMode("mixdown")); CHECK(vm.setActiveMode("mixdown"));
std::string json = vm.serialize(); std::string json = vm.serialize();
auto back = ViewModel::deserialize(json); auto back = ViewModeModel::deserialize(json);
CHECK(back.has_value()); CHECK(back.has_value());
CHECK(back && *back == vm); CHECK(back && *back == vm);
// String form is stable across a second round-trip. // String form is stable across a second round-trip.
@@ -323,14 +324,14 @@ static void testJsonRoundTrip() {
} }
static void testEmptyModelRoundTrip() { static void testEmptyModelRoundTrip() {
ViewModel vm; // default: Arrange + Design seeded, active = Arrange, no members ViewModeModel vm; // default: Arrange + Design seeded, active = Arrange, no members
std::string json = vm.serialize(); std::string json = vm.serialize();
auto back = ViewModel::deserialize(json); auto back = ViewModeModel::deserialize(json);
CHECK(back.has_value()); CHECK(back.has_value());
CHECK(back && *back == vm); CHECK(back && *back == vm);
// Lenient empty root ⇒ a default-seeded model. // Lenient empty root ⇒ a default-seeded model.
auto empty = ViewModel::deserialize("{}"); auto empty = ViewModeModel::deserialize("{}");
CHECK(empty.has_value()); CHECK(empty.has_value());
CHECK(empty && empty->modes().size() == 2); CHECK(empty && empty->modes().size() == 2);
CHECK(empty && empty->activeModeId() == kArrangeModeId); CHECK(empty && empty->activeModeId() == kArrangeModeId);
@@ -352,11 +353,36 @@ static void testMalformedJson() {
"{\"modes\":[]}trailing", // trailing garbage "{\"modes\":[]}trailing", // trailing garbage
}; };
for (const char* j : bad) { for (const char* j : bad) {
auto r = ViewModel::deserialize(j); auto r = ViewModeModel::deserialize(j);
CHECK(!r.has_value()); 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() { int main() {
testNModeRegistryAndMembership(); testNModeRegistryAndMembership();
testParentDerivationMultiMode(); testParentDerivationMultiMode();
@@ -366,6 +392,7 @@ int main() {
testJsonRoundTrip(); testJsonRoundTrip();
testEmptyModelRoundTrip(); testEmptyModelRoundTrip();
testMalformedJson(); testMalformedJson();
testPlanToggleParkHasEmptyFxOffline();
if (g_fail == 0) std::printf("All tests passed.\n"); if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0; return g_fail ? 1 : 0;