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/app_settings.h"
17#include "utils/io_utils.h"
18
19#include "helpers/mock_hardware_audio_interface.h"
20#include "helpers/mock_settings_backend.h"
21#include "helpers/project_json_comparators.h"
22#include "helpers/scoped_juce_qapplication.h"
23
24#include <gtest/gtest.h>
25#include <nlohmann/json.hpp>
26
27namespace zrythm::controllers
28{
29
30// ============================================================================
31// Helper Functions
32// ============================================================================
33
35inline nlohmann::json
36create_minimal_valid_project_json ()
37{
38 nlohmann::json j;
39
40 // Top-level required fields
41 j["documentType"] = "ZrythmProject";
42 j["schemaVersion"] = nlohmann::json::object ();
43 j["schemaVersion"]["major"] = 2;
44 j["schemaVersion"]["minor"] = 1;
45 j["appVersion"] = nlohmann::json::object ();
46 j["appVersion"]["major"] = 2;
47 j["appVersion"]["minor"] = 0;
48 j["appVersion"]["patch"] = 0;
49 j["datetime"] = "2026-02-16T12:00:00Z";
50 j["title"] = "Test Project";
51
52 // projectData section
53 auto &pd = j["projectData"];
54
55 // tempoMap (required)
56 pd["tempoMap"] = nlohmann::json::object ();
57 pd["tempoMap"]["timeSignatures"] = nlohmann::json::array ();
58 pd["tempoMap"]["tempoChanges"] = nlohmann::json::array ();
59
60 // transport (required)
61 pd["transport"] = nlohmann::json::object ();
62
63 // tracklist (required)
64 pd["tracklist"] = nlohmann::json::object ();
65 pd["tracklist"]["tracks"] = nlohmann::json::array ();
66 pd["tracklist"]["pinnedTracksCutoff"] = 0;
67 pd["tracklist"]["trackRoutes"] = nlohmann::json::array ();
68
69 // registries (required)
70 pd["registries"] = nlohmann::json::object ();
71 pd["registries"]["portRegistry"] = nlohmann::json::array ();
72 pd["registries"]["paramRegistry"] = nlohmann::json::array ();
73 pd["registries"]["pluginRegistry"] = nlohmann::json::array ();
74 pd["registries"]["trackRegistry"] = nlohmann::json::array ();
75 pd["registries"]["arrangerObjectRegistry"] = nlohmann::json::array ();
76 pd["registries"]["fileAudioSourceRegistry"] = nlohmann::json::array ();
77
78 return j;
79}
80
82inline std::set<std::string>
83extract_all_uuids (const nlohmann::json &j)
84{
85 std::set<std::string> uuids;
86
87 const auto extract = [&] (auto &&self, const nlohmann::json &obj) -> void {
88 if (obj.is_object ())
89 {
90 if (obj.contains ("id") && obj["id"].is_string ())
91 {
92 uuids.insert (obj["id"].get<std::string> ());
93 }
94 for (const auto &[key, val] : obj.items ())
95 {
96 self (self, val);
97 }
98 }
99 else if (obj.is_array ())
100 {
101 for (const auto &elem : obj)
102 {
103 self (self, elem);
104 }
105 }
106 };
107
108 extract (extract, j);
109 return uuids;
110}
111
113inline size_t
114count_objects (const nlohmann::json &j)
115{
116 size_t count = 0;
117
118 const auto count_fn = [&] (auto &&self, const nlohmann::json &obj) -> void {
119 if (obj.is_object ())
120 {
121 ++count;
122 for (const auto &[key, val] : obj.items ())
123 {
124 self (self, val);
125 }
126 }
127 else if (obj.is_array ())
128 {
129 for (const auto &elem : obj)
130 {
131 self (self, elem);
132 }
133 }
134 };
135
136 count_fn (count_fn, j);
137 return count;
138}
139
140// ============================================================================
141// Test Fixture
142// ============================================================================
143
146 : public ::testing::Test,
148{
149protected:
150 void SetUp () override
151 {
152 // Create a temporary directory for the project
153 temp_dir_obj = utils::io::make_tmp_dir ();
154 project_dir =
155 utils::Utf8String::from_qstring (temp_dir_obj->path ()).to_path ();
156
157 hw_interface = std::make_unique<test_helpers::MockHardwareAudioInterface> ();
158
159 plugin_format_manager = std::make_shared<juce::AudioPluginFormatManager> ();
160 juce::addDefaultFormatsToManager (*plugin_format_manager);
161
162 // Create a mock settings backend
163 auto mock_backend = std::make_unique<test_helpers::MockSettingsBackend> ();
164 mock_backend_ptr = mock_backend.get ();
165
166 // Set up default expectations for common settings
167 ON_CALL (*mock_backend_ptr, value (testing::_, testing::_))
168 .WillByDefault (testing::Return (QVariant ()));
169
170 app_settings =
171 std::make_unique<utils::AppSettings> (std::move (mock_backend));
172
173 // Create port registry and monitor fader
174 port_registry = std::make_unique<dsp::PortRegistry> (nullptr);
175 param_registry = std::make_unique<dsp::ProcessorParameterRegistry> (
176 *port_registry, nullptr);
177 monitor_fader = utils::make_qobject_unique<dsp::Fader> (
179 .port_registry_ = *port_registry,
180 .param_registry_ = *param_registry,
181 },
182 dsp::PortType::Audio,
183 true, // hard_limit_output
184 false, // make_params_automatable
185 [] () -> utils::Utf8String { return u8"Test Control Room"; },
186 [] (bool fader_solo_status) { return false; });
187
188 // Create metronome with test samples
189 juce::AudioSampleBuffer emphasis_sample (2, 512);
190 juce::AudioSampleBuffer normal_sample (2, 512);
191 metronome = utils::make_qobject_unique<dsp::Metronome> (
193 .port_registry_ = *port_registry,
194 .param_registry_ = *param_registry,
195 },
196 emphasis_sample, normal_sample, true, 1.0f, nullptr);
197 }
198
199 void TearDown () override
200 {
201 undo_stack.reset ();
202 ui_state.reset ();
203 metronome.reset ();
204 monitor_fader.reset ();
205 param_registry.reset ();
206 port_registry.reset ();
207 app_settings.reset ();
208 plugin_format_manager.reset ();
209 hw_interface.reset ();
210 }
211
212 std::unique_ptr<structure::project::Project> create_minimal_project ()
213 {
214 structure::project::Project::ProjectDirectoryPathProvider path_provider =
215 [this] (bool for_backup) {
216 if (for_backup)
217 {
218 return project_dir / "backups";
219 }
220 return project_dir;
221 };
222
223 plugins::PluginHostWindowFactory window_factory =
224 [] (plugins::Plugin &) -> std::unique_ptr<plugins::IPluginHostWindow> {
225 return nullptr;
226 };
227
228 auto project = std::make_unique<structure::project::Project> (
229 *app_settings, path_provider, *hw_interface, plugin_format_manager,
230 window_factory, *metronome, *monitor_fader);
231
232 return project;
233 }
234
235 void create_ui_state_and_undo_stack (structure::project::Project &project)
236 {
237 ui_state = utils::make_qobject_unique<structure::project::ProjectUiState> (
238 project, *app_settings);
239
240 undo_stack = utils::make_qobject_unique<undo::UndoStack> (
241 [&project] (const std::function<void ()> &action, bool recalculate_graph) {
243 action, recalculate_graph);
244 },
245 nullptr);
246 }
247
248 // Helper to verify a UUID string format
249 static void assert_valid_uuid (const std::string &uuid_str)
250 {
251 // UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
252 static const std::regex uuid_regex (
253 R"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
254 std::regex_constants::icase);
255 EXPECT_TRUE (std::regex_match (uuid_str, uuid_regex))
256 << "Invalid UUID format: " << uuid_str;
257 }
258
259 // Helper to verify a hex color format (#RRGGBB)
260 static void assert_valid_color (const std::string &color_str)
261 {
262 static const std::regex color_regex (R"(#[0-9A-Fa-f]{6})");
263 EXPECT_TRUE (std::regex_match (color_str, color_regex))
264 << "Invalid color format: " << color_str;
265 }
266
267 static constexpr utils::Version TEST_APP_VERSION{ 2, 0, {} };
268 static constexpr utils::Version TEST_APP_VERSION_WITH_PATCH{ 2, 0, 1 };
269
270 std::unique_ptr<QTemporaryDir> temp_dir_obj;
271 std::filesystem::path project_dir;
272 std::unique_ptr<dsp::IHardwareAudioInterface> hw_interface;
273 std::shared_ptr<juce::AudioPluginFormatManager> plugin_format_manager;
274 test_helpers::MockSettingsBackend * mock_backend_ptr{};
275 std::unique_ptr<utils::AppSettings> app_settings;
276 std::unique_ptr<dsp::PortRegistry> port_registry;
277 std::unique_ptr<dsp::ProcessorParameterRegistry> param_registry;
282};
283
284} // 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:46
Core functionality of a Zrythm project.
Definition project.h:51
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:36
Lightweight UTF-8 string wrapper with safe conversions.
Definition utf8_string.h:37
Represents a semantic version with major, minor, and optional patch.
Definition version.h:29