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