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