Zrythm v2.0.0-DEV
a highly automated and intuitive digital audio workstation
Loading...
Searching...
No Matches
project_json_serializer_test.h
1// SPDX-FileCopyrightText: © 2026 Alexandros Theodotou <alex@zrythm.org>
2// SPDX-License-Identifier: LicenseRef-ZrythmLicense
3
4#pragma once
5
6#include <functional>
7#include <memory>
8#include <regex>
9#include <set>
10#include <string>
11
12#include "controllers/project_json_serializer.h"
13#include "structure/project/project.h"
14#include "structure/project/project_ui_state.h"
15#include "undo/undo_stack.h"
16#include "utils/io_utils.h"
17
18#include "helpers/mock_hardware_audio_interface.h"
19#include "helpers/mock_settings_backend.h"
20#include "helpers/scoped_juce_qapplication.h"
21
22#include <gtest/gtest.h>
23#include <nlohmann/json.hpp>
24
25namespace zrythm::controllers
26{
27
28// ============================================================================
29// Helper Functions
30// ============================================================================
31
33inline nlohmann::json
34create_minimal_valid_project_json ()
35{
36 nlohmann::json j;
37
38 // Top-level required fields
39 j["documentType"] = "ZrythmProject";
40 j["schemaVersion"] = nlohmann::json::object ();
41 j["schemaVersion"]["major"] = 2;
42 j["schemaVersion"]["minor"] = 1;
43 j["appVersion"] = nlohmann::json::object ();
44 j["appVersion"]["major"] = 2;
45 j["appVersion"]["minor"] = 0;
46 j["appVersion"]["patch"] = 0;
47 j["datetime"] = "2026-02-16T12:00:00Z";
48 j["title"] = "Test Project";
49
50 // projectData section
51 auto &pd = j["projectData"];
52
53 // tempoMap (required)
54 pd["tempoMap"] = nlohmann::json::object ();
55 pd["tempoMap"]["timeSignatures"] = nlohmann::json::array ();
56 pd["tempoMap"]["tempoChanges"] = nlohmann::json::array ();
57
58 // transport (required)
59 pd["transport"] = nlohmann::json::object ();
60
61 // tracklist (required)
62 pd["tracklist"] = nlohmann::json::object ();
63 pd["tracklist"]["tracks"] = nlohmann::json::array ();
64 pd["tracklist"]["pinnedTracksCutoff"] = 0;
65 pd["tracklist"]["trackRoutes"] = nlohmann::json::array ();
66
67 // registries (required)
68 pd["registries"] = nlohmann::json::object ();
69 pd["registries"]["portRegistry"] = nlohmann::json::array ();
70 pd["registries"]["paramRegistry"] = nlohmann::json::array ();
71 pd["registries"]["pluginRegistry"] = nlohmann::json::array ();
72 pd["registries"]["trackRegistry"] = nlohmann::json::array ();
73 pd["registries"]["arrangerObjectRegistry"] = nlohmann::json::array ();
74 pd["registries"]["fileAudioSourceRegistry"] = nlohmann::json::array ();
75
76 return j;
77}
78
80inline std::set<std::string>
81extract_all_uuids (const nlohmann::json &j)
82{
83 std::set<std::string> uuids;
84
85 const auto extract = [&] (auto &&self, const nlohmann::json &obj) -> void {
86 if (obj.is_object ())
87 {
88 if (obj.contains ("id") && obj["id"].is_string ())
89 {
90 uuids.insert (obj["id"].get<std::string> ());
91 }
92 for (const auto &[key, val] : obj.items ())
93 {
94 self (self, val);
95 }
96 }
97 else if (obj.is_array ())
98 {
99 for (const auto &elem : obj)
100 {
101 self (self, elem);
102 }
103 }
104 };
105
106 extract (extract, j);
107 return uuids;
108}
109
112inline void
113expect_registries_match (const nlohmann::json &j1, const nlohmann::json &j2)
114{
115 const auto &regs1 = j1["projectData"]["registries"];
116 const auto &regs2 = j2["projectData"]["registries"];
117
118 const std::array<std::string_view, 5> registry_names = {
119 // "portRegistry",
120 "paramRegistry", "pluginRegistry", "trackRegistry",
121 "arrangerObjectRegistry", "fileAudioSourceRegistry",
122 };
123
124 for (const auto &name : registry_names)
125 {
126 const auto &reg1 = regs1[name];
127 const auto &reg2 = regs2[name];
128
129 EXPECT_EQ (reg1.size (), reg2.size ())
130 << "Registry " << name << " size mismatch";
131
132 // Build map of ID -> object for each registry
133 std::map<std::string, nlohmann::json> objs1, objs2;
134 for (const auto &obj : reg1)
135 objs1[obj["id"].get<std::string> ()] = obj;
136 for (const auto &obj : reg2)
137 objs2[obj["id"].get<std::string> ()] = obj;
138
139 // Find first mismatch
140 for (const auto &[id, obj] : objs1)
141 {
142 if (!objs2.contains (id))
143 {
144 FAIL ()
145 << "Registry " << name << ": object from j1 missing in j2:\n"
146 << obj.dump (2);
147 }
148 }
149 for (const auto &[id, obj] : objs2)
150 {
151 if (!objs1.contains (id))
152 {
153 FAIL ()
154 << "Registry " << name << ": object from j2 missing in j1:\n"
155 << obj.dump (2);
156 }
157 }
158 }
159}
160
162inline size_t
163count_objects (const nlohmann::json &j)
164{
165 size_t count = 0;
166
167 const auto count_fn = [&] (auto &&self, const nlohmann::json &obj) -> void {
168 if (obj.is_object ())
169 {
170 ++count;
171 for (const auto &[key, val] : obj.items ())
172 {
173 self (self, val);
174 }
175 }
176 else if (obj.is_array ())
177 {
178 for (const auto &elem : obj)
179 {
180 self (self, elem);
181 }
182 }
183 };
184
185 count_fn (count_fn, j);
186 return count;
187}
188
189// ============================================================================
190// Test Fixture
191// ============================================================================
192
195 : public ::testing::Test,
197{
198protected:
199 void SetUp () override
200 {
201 // Create a temporary directory for the project
202 temp_dir_obj = utils::io::make_tmp_dir ();
203 project_dir =
204 utils::Utf8String::from_qstring (temp_dir_obj->path ()).to_path ();
205
206 hw_interface = std::make_unique<test_helpers::MockHardwareAudioInterface> ();
207
208 plugin_format_manager = std::make_shared<juce::AudioPluginFormatManager> ();
209 juce::addDefaultFormatsToManager (*plugin_format_manager);
210
211 // Create a mock settings backend
212 auto mock_backend = std::make_unique<test_helpers::MockSettingsBackend> ();
213 mock_backend_ptr = mock_backend.get ();
214
215 // Set up default expectations for common settings
216 ON_CALL (*mock_backend_ptr, value (testing::_, testing::_))
217 .WillByDefault (testing::Return (QVariant ()));
218
219 app_settings =
220 std::make_unique<utils::AppSettings> (std::move (mock_backend));
221
222 // Create port registry and monitor fader
223 port_registry = std::make_unique<dsp::PortRegistry> (nullptr);
224 param_registry = std::make_unique<dsp::ProcessorParameterRegistry> (
225 *port_registry, nullptr);
226 monitor_fader = utils::make_qobject_unique<dsp::Fader> (
228 .port_registry_ = *port_registry,
229 .param_registry_ = *param_registry,
230 },
231 dsp::PortType::Audio,
232 true, // hard_limit_output
233 false, // make_params_automatable
234 [] () -> utils::Utf8String { return u8"Test Control Room"; },
235 [] (bool fader_solo_status) { return false; });
236
237 // Create metronome with test samples
238 juce::AudioSampleBuffer emphasis_sample (2, 512);
239 juce::AudioSampleBuffer normal_sample (2, 512);
240 metronome = utils::make_qobject_unique<dsp::Metronome> (
242 .port_registry_ = *port_registry,
243 .param_registry_ = *param_registry,
244 },
245 emphasis_sample, normal_sample, true, 1.0f, nullptr);
246 }
247
248 void TearDown () override
249 {
250 undo_stack.reset ();
251 ui_state.reset ();
252 metronome.reset ();
253 monitor_fader.reset ();
254 param_registry.reset ();
255 port_registry.reset ();
256 app_settings.reset ();
257 plugin_format_manager.reset ();
258 hw_interface.reset ();
259 }
260
261 std::unique_ptr<structure::project::Project> create_minimal_project ()
262 {
263 structure::project::Project::ProjectDirectoryPathProvider path_provider =
264 [this] (bool for_backup) {
265 if (for_backup)
266 {
267 return project_dir / "backups";
268 }
269 return project_dir;
270 };
271
272 plugins::PluginHostWindowFactory window_factory =
273 [] (plugins::Plugin &) -> std::unique_ptr<plugins::IPluginHostWindow> {
274 return nullptr;
275 };
276
277 auto project = std::make_unique<structure::project::Project> (
278 *app_settings, path_provider, *hw_interface, plugin_format_manager,
279 window_factory, *metronome, *monitor_fader);
280
281 return project;
282 }
283
284 void create_ui_state_and_undo_stack (structure::project::Project &project)
285 {
286 ui_state = utils::make_qobject_unique<structure::project::ProjectUiState> (
287 project, *app_settings);
288
289 undo_stack = utils::make_qobject_unique<undo::UndoStack> (
290 [&project] (const std::function<void ()> &action, bool recalculate_graph) {
292 action, recalculate_graph);
293 },
294 nullptr);
295 }
296
297 // Helper to verify a UUID string format
298 static void assert_valid_uuid (const std::string &uuid_str)
299 {
300 // UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
301 static const std::regex uuid_regex (
302 R"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
303 std::regex_constants::icase);
304 EXPECT_TRUE (std::regex_match (uuid_str, uuid_regex))
305 << "Invalid UUID format: " << uuid_str;
306 }
307
308 // Helper to verify a hex color format (#RRGGBB)
309 static void assert_valid_color (const std::string &color_str)
310 {
311 static const std::regex color_regex (R"(#[0-9A-Fa-f]{6})");
312 EXPECT_TRUE (std::regex_match (color_str, color_regex))
313 << "Invalid color format: " << color_str;
314 }
315
316 static constexpr utils::Version TEST_APP_VERSION{ 2, 0, {} };
317 static constexpr utils::Version TEST_APP_VERSION_WITH_PATCH{ 2, 0, 1 };
318
319 std::unique_ptr<QTemporaryDir> temp_dir_obj;
320 fs::path project_dir;
321 std::unique_ptr<dsp::IHardwareAudioInterface> hw_interface;
322 std::shared_ptr<juce::AudioPluginFormatManager> plugin_format_manager;
323 test_helpers::MockSettingsBackend * mock_backend_ptr{};
324 std::unique_ptr<utils::AppSettings> app_settings;
325 std::unique_ptr<dsp::PortRegistry> port_registry;
326 std::unique_ptr<dsp::ProcessorParameterRegistry> param_registry;
331};
332
333} // namespace zrythm::controllers
Fixture for testing Project serialization with a minimal Project setup.
void execute_function_with_paused_processing_synchronously(const std::function< void()> &func, bool recalculate_graph)
Executes the given function after pausing processing and then resumes processing.
DSP processing plugin.
Definition plugin.h:30
Core functionality of a Zrythm project.
Definition project.h:48
QApplication wrapper that also spins the JUCE message loop.
A unique pointer for QObject objects that also works with QObject-based ownership.
Definition qt.h:38
Lightweight UTF-8 string wrapper with safe conversions.
Definition utf8_string.h:38
Represents a semantic version with major, minor, and optional patch.
Definition version.h:29