11 {
"Project.is_playing",
"Transport state, playback has no undo." },
12 {
"Track.muted",
"Tracktion's AudioTrack binds IDs::mute without the undo manager." },
13 {
"Track.solo",
"Tracktion's AudioTrack binds IDs::solo without the undo manager." },
18struct Snapshot { Values values, properties; };
19static const String test_note =
"{\"note_number\":60,\"start_beat\":0.25,\"length_beats\":0.5,\"velocity\":90}";
22normalize_refs (Jsonipc::JsonValue &value)
25 for (
auto &element : value.GetArray())
26 normalize_refs (element);
27 else if (value.IsObject())
29 for (
auto &member : value.GetObject())
30 if (member.name ==
"$id")
31 member.value.SetInt (0);
33 normalize_refs (member.value);
42 ProjectImplP project = ProjectImpl::create (
"PropertyLoop");
50 TrackP track = project->create_track();
51 ClipP midi_clip = track->create_midi_clip (
"MidiClip", 1, 2);
52 audio_clip = track->create_audio_clip (
"AudioClip", 4, 2);
53 PluginP plugin = track->create_plugin (
"4osc");
54 PluginP text_plugin = track->create_plugin (
"text");
55 PluginP insert_plugin = track->create_plugin (
"insert");
56 add (
"Project", ProjectP (project));
57 add (
"MasterTrack", project->master_track());
59 add (
"MidiClip", midi_clip);
60 add (
"AudioClip", audio_clip);
61 add (
"Plugin", plugin);
62 add (
"TextPlugin", text_plugin);
63 add (
"InsertPlugin", insert_plugin);
64 for (TrackP t : project->all_tracks())
66 add (t->is_control_track() && t->name() ==
"Chord" ?
"ChordTrack" :
"", t);
67 for (ClipP c : t->launcher_clips())
69 for (PluginP p : t->list_plugins())
72 call (
"change_batch", ref (
"MidiClip"),
"[" + test_note +
"],\"Initial Note\"");
73 project->clear_undo();
80 template<
class T>
const Object&
84 const bool assign = !label.
empty();
86 rapidjson::Document doc;
87 const Jsonipc::JsonValue
ref = Jsonipc::to_json (obj, doc.GetAllocator());
88 const String self = Jsonipc::jsonvalue_to_string (ref);
89 for (
const auto &o : objects)
94 label = Jsonipc::short_name (wrapper->classname()) +
"#" +
std::to_string (objects.size());
95 const StringS names = wrapper->list_methods();
97 for (
const String &name : names)
99 props.push_back (name.substr (4));
100 connections.push_back (obj->on_event (
"notify", [
this, self] (
const Event &event) {
101 notified.insert (self +
"." + event.detail());
103 objects.push_back ({ label, self, props, obj->read_only_properties(), assign });
104 return objects.back();
111 const String msg =
"{\"id\":1,\"method\":\"" + method +
"\",\"params\":[" + self + (arg.empty() ?
"" :
"," + arg) +
"]}";
112 const String reply = dispatcher.dispatch_message (msg);
113 rapidjson::Document doc;
114 doc.Parse (reply.c_str());
115 if (doc.HasParseError() || !doc.HasMember (
"result"))
116 printerr (
"%s: %s\n", msg, reply);
117 TASSERT (!doc.HasParseError() && doc.HasMember (
"result"));
118 return Jsonipc::jsonvalue_to_string (doc[
"result"]);
123 for (
const auto &o : objects)
124 if (o.label == label)
131 const String key = self +
"." + prop, value = call (method, self);
132 property_labels[key] = label +
"." + prop;
134 snapshot.properties[key] = value;
135 rapidjson::Document doc;
136 doc.Parse (value.c_str());
137 normalize_refs (doc);
138 snapshot.values[label +
"." + prop] = Jsonipc::jsonvalue_to_string (doc);
143 const Object &o = add (
"", obj);
144 for (
const auto &p : o.props)
145 read (snapshot, label, o.self, p,
"get/" + p, true);
152 read_object (values,
"Project", ProjectP (project));
153 read (values,
"Project", ref (
"Project"),
"all_tracks",
"all_tracks");
155 for (
const auto &track : project->all_tracks())
158 const String self = read_object (values, label, track);
159 read (values, label, self,
"list_plugins",
"list_plugins");
160 size_t ci = 0, pi = 0;
161 for (
const auto &clip : track->launcher_clips())
164 const String cself = read_object (values, clabel, clip);
165 read (values, clabel, cself,
"notes",
"list_all_notes",
true);
167 for (
const auto &plugin : track->list_plugins())
168 read_object (values,
string_format (
"%s.Plugin[%u]", label,
uint (pi++)), plugin);
170 read_object (values,
"AudioClip", audio_clip);
177 {
"MidiClip.all_notes", [] (LoopProject &lp) {
178 lp.call (
"set/all_notes", lp.ref (
"MidiClip"),
"[{\"note_number\":67,\"length_beats\":1,\"velocity\":45}]");
180 {
"MidiClip.position", [] (LoopProject &lp) {
181 lp.call (
"set/position", lp.ref (
"MidiClip"),
"{\"time\":{\"start\":1.25,\"end\":3.25},\"offset\":0.5}");
183 {
"AudioClip.position", [] (LoopProject &lp) {
184 lp.call (
"set/position", lp.ref (
"AudioClip"),
"{\"time\":{\"start\":4.25,\"end\":6.25},\"offset\":0.5}");
190nudge (
const String &v,
int dir)
196 if (!v.
empty() && v[0] ==
'"')
204changed (
const Values &a,
const Values &b)
207 for (
const auto &[k, v] : a)
208 if (!b.
count (k) || b.at (k) != v)
210 for (
const auto &[k, v] : b)
217check_undo (LoopProject &lp,
const Snapshot &before,
const Snapshot &after)
220 auto require_notifies = [&] (
const char *step,
const Snapshot &a,
const Snapshot &b) {
221 for (
const auto &[key, value] : a.properties)
222 if (b.properties.
count (key) && b.properties.at (key) != value && !lp.notified.
count (key))
223 problems.push_back (
string_format (
"%s: %s changed without notify", step, lp.property_labels.at (key)));
225 if (before.values == after.values)
227 if (!lp.project->can_undo())
228 problems.
push_back (
"set: adds no undo step");
229 require_notifies (
"set", before, after);
232 if (lp.project->can_undo())
233 problems.
push_back (
"undo: more than one undo step");
234 const Snapshot undone = lp.snapshot();
235 for (
const auto &k : changed (before.values, undone.values))
236 problems.push_back (
"undo: " + k +
" not restored");
237 require_notifies (
"undo", after, undone);
240 if (lp.project->can_redo())
241 problems.
push_back (
"redo: more than one redo step");
242 const Snapshot redone = lp.snapshot();
243 for (
const auto &k : changed (after.values, redone.values))
244 problems.push_back (
"redo: " + k +
" not re-applied");
245 require_notifies (
"redo", undone, redone);
251check_property (
const String &key,
int dir = +1)
255 const Snapshot before = lp.snapshot();
256 const String target = lp.ref (label) +
"." + prop;
257 const String old = before.properties.at (target), value = nudge (old, dir);
259 if (property_changes.count (key))
260 property_changes.at (key) (lp);
262 lp.call (
"set/" + prop, lp.ref (label), value);
263 const Snapshot after = lp.snapshot();
264 if (after.properties.at (target) == old && dir > 0)
266 StringS problems = check_property (key, -1);
267 for (
const auto &k : changed (before.values, after.values))
268 problems.push_back (
string_format (
"set %s: changes %s, the property stays %s", value, k, old));
271 StringS problems = check_undo (lp, before, after);
272 if (after.properties.at (target) == old)
273 problems.
push_back (
"set: no effect, still " + old);
283 for (
const auto &o : lp.objects)
285 for (
const auto &p : o.read_only)
287 failures.push_back (o.label +
"." + p +
": read-only, but there is no such property");
288 for (
const auto &p : o.props)
290 const String key = o.label +
"." + p;
292 if (o.assign && !
std::count (o.read_only.begin(), o.read_only.end(), p))
294 if (property_changes.count (key) || !nudge (lp.call (
"get/" + p, o.self), +1).empty())
297 failures.
push_back (key +
": add a test lambda to property_changes");
302 for (
const auto &[key, reason] : exempt_properties)
304 failures.push_back (key +
": not assigned, remove it from exempt_properties");
305 for (
const auto &[key, change] : property_changes)
307 failures.push_back (key +
": not assigned, remove it from property_changes");
308 for (
const auto &key : checks)
310 const StringS problems = check_property (key);
311 if (!exempt_properties.count (key))
312 for (
const auto &p : problems)
313 failures.push_back (key +
": " + p);
314 else if (problems.
empty())
315 failures.
push_back (key +
": passes, remove it from exempt_properties");
317 for (
const auto &f : failures)
318 printerr (
"%s\n", f);
324 {
"Project.create_track", [] (LoopProject &lp) { lp.call (
"create_track", lp.ref (
"Project")); } },
325 {
"Project.group_undo", [] (LoopProject &lp) {
326 lp.call (
"group_undo", lp.ref (
"Project"),
"\"Rename Clips\"");
327 lp.call (
"set/name", lp.ref (
"MidiClip"),
"\"Renamed MIDI\"");
328 lp.call (
"set/name", lp.ref (
"AudioClip"),
"\"Renamed Audio\"");
329 lp.call (
"ungroup_undo", lp.ref (
"Project"));
331 {
"Track.create_midi_clip", [] (LoopProject &lp) {
332 lp.call (
"create_midi_clip", lp.ref (
"Track"),
"\"New MIDI Clip\",1,2");
334 {
"Track.create_audio_clip", [] (LoopProject &lp) {
335 lp.call (
"create_audio_clip", lp.ref (
"Track"),
"\"New Audio Clip\",8,2");
337 {
"Track.create_plugin", [] (LoopProject &lp) { lp.call (
"create_plugin", lp.ref (
"Track"),
"\"4osc\""); } },
338 {
"Gadget.remove_self/Track", [] (LoopProject &lp) { lp.call (
"remove_self", lp.ref (
"Track")); } },
339 {
"Gadget.remove_self/MidiClip", [] (LoopProject &lp) { lp.call (
"remove_self", lp.ref (
"MidiClip")); } },
340 {
"Gadget.remove_self/AudioClip", [] (LoopProject &lp) { lp.call (
"remove_self", lp.ref (
"AudioClip")); } },
341 {
"Gadget.remove_self/Plugin", [] (LoopProject &lp) { lp.call (
"remove_self", lp.ref (
"Plugin")); } },
345 {
"Clip.change_batch",
"Special note-editing flow, deferred to its planned rewrite in #141." },
346 {
"Track.create_audio_clip",
"Audio clips have no API list, so the loop sees no change." },
347 {
"Gadget.remove_self/Track",
"Audio clip handles cannot be reacquired on undo." },
348 {
"Gadget.remove_self/AudioClip",
"Audio clips have no API list, so restored clips cannot be reacquired." },
349 {
"Gadget.remove_self/Plugin",
"Undo reuses the plugin's invalidated API handle." },
350 {
"Emittable.emit_event",
"Event delivery, not project data." },
351 {
"Emittable.emit_notify",
"Event delivery, not project data." },
352 {
"Emittable.js_trigger",
"Event subscription, not project data." },
353 {
"Gadget.type_nick",
"Type query." },
354 {
"Gadget.list_properties",
"Property metadata query." },
355 {
"Gadget.access_property",
"Property handle query." },
356 {
"Gadget.access_properties",
"Property handle query." },
357 {
"Gadget.get_value",
"Property value query." },
358 {
"Gadget.set_value",
"Uses Property handles; these objects expose no writable Property handles yet." },
359 {
"Gadget.read_only_properties",
"Property metadata query." },
360 {
"Gadget.set_data",
"Session data, intentionally outside the Edit and undo." },
361 {
"Gadget.get_data",
"Session data query." },
362 {
"Device.device_info",
"Device metadata query." },
363 {
"Plugin.plugin_type",
"Plugin type query." },
364 {
"Clip.list_all_notes",
"Query, compared as notes after every change." },
365 {
"Clip.telemetry",
"Telemetry memory location query." },
366 {
"Track.is_master",
"Track type query." },
367 {
"Track.is_control_track",
"Track type query." },
368 {
"Track.list_plugins",
"Query, compared after every change." },
369 {
"Track.access_device",
"Device handle query, currently returns nullptr." },
370 {
"Track.create_monitor",
"Unimplemented, currently returns nullptr." },
371 {
"Track.telemetry",
"Telemetry memory location query." },
372 {
"Monitor.get_output",
"Output device query." },
373 {
"Monitor.get_ochannel",
"Output channel query." },
374 {
"Monitor.get_mix_freq",
"Sample rate query." },
375 {
"Monitor.get_frame_duration",
"Frame duration query." },
376 {
"Project.discard",
"Project lifetime, not an undoable edit." },
377 {
"Project.start_playback",
"Transport state, playback has no undo." },
378 {
"Project.pause_playback",
"Transport state, playback has no undo." },
379 {
"Project.stop_playback",
"Transport state, playback has no undo." },
380 {
"Project.all_tracks",
"Query, compared after every change." },
381 {
"Project.master_track",
"Master track handle query." },
382 {
"Project.save_project",
"File I/O, covered by step 7." },
383 {
"Project.saved_filename",
"File name query." },
384 {
"Project.load_project",
"File I/O, covered by step 7." },
385 {
"Project.telemetry",
"Telemetry memory location query." },
386 {
"Project.group_undo",
"Clip setters and change_batch start their own transactions, splitting explicit undo groups." },
387 {
"Project.ungroup_undo",
"Undo history control, not a project mutation." },
388 {
"Project.undo",
"Undo history control, exercised by the loop." },
389 {
"Project.can_undo",
"Undo history query, exercised by the loop." },
390 {
"Project.redo",
"Undo history control, exercised by the loop." },
391 {
"Project.can_redo",
"Undo history query, exercised by the loop." },
392 {
"Project.length",
"Query, read-only property coverage belongs to step 6." },
393 {
"Project.match_serialized",
"Serialized state query." },
396template<
class T,
class Base>
static void
400 for (
const auto &name : Jsonipc::Class<T>::list_methods())
402 methods.insert (cls +
"." + name);
410 list_methods<Emittable, SharedBase> (
"Emittable", methods);
411 list_methods<Object, Emittable> (
"Object", methods);
412 list_methods<Gadget, Object> (
"Gadget", methods);
413 list_methods<Device, Gadget> (
"Device", methods);
414 list_methods<Project, Device> (
"Project", methods);
415 list_methods<Track, Device> (
"Track", methods);
416 list_methods<Clip, Gadget> (
"Clip", methods);
417 list_methods<Plugin, Device> (
"Plugin", methods);
418 list_methods<Monitor, Gadget> (
"Monitor", methods);
420 for (
const auto &key : methods)
421 if (!method_checks.
count (key) && !exempt_methods.
count (key) &&
422 !
std::
any_of (method_checks.
begin(), method_checks.
end(), [&] (const auto &entry) {
425 failures.
push_back (key +
": add a test lambda to method_checks or an exempt_methods line with a reason");
426 for (
const auto &[key, reason] : exempt_methods)
427 if ((!methods.
count (key) && !method_checks.
count (key)) || reason.
empty())
428 failures.push_back (key +
": remove the stale exemption or give a reason");
429 for (
const auto &[key, change] : method_checks)
431 if (!methods.count (key.
substr (0, key.
find (
'/'))) || !change)
433 failures.
push_back (key +
": no such method or no test lambda");
437 const Snapshot before = lp.snapshot();
440 const Snapshot after = lp.snapshot();
441 const StringS problems = check_undo (lp, before, after);
442 if (!exempt_methods.count (key))
443 for (
const auto &problem : problems)
444 failures.push_back (key +
": " + problem);
445 else if (problems.
empty())
446 failures.
push_back (key +
": passes, remove it from exempt_methods");
448 for (
const auto &failure : failures)
449 printerr (
"%s\n", failure);
Base type for classes with Property interfaces.
Maps C++ shared_ptr instances to JSON-wrapped objects with unique IDs, supporting polymorphic upcasti...
Keep track of temporary instances during IpcDispatcher::dispatch_message().
The Anklang C++ API namespace.
std::string string_format(const char *format, const Args &...args) __attribute__((__format__(__printf__
Format a string similar to sprintf(3) with support for std::string and std::ostringstream convertible...
double string_to_double(const String &string)
Parse a double from a string, trying locale specific characters and POSIX/C formatting.
uint32_t uint
Provide 'uint' as convenience type.
bool string_startswith(const String &string, const String &fragment)
Returns whether string starts with fragment.
Structure for callback based notifications.
static std::vector< std::string > list_methods()
List the method names registered for T and its base classes, accessors as "get/name" and "set/name".
#define TASSERT(cond)
Unconditional test assertion, enters breakpoint if not fullfilled.
#define TEST_ADD(fun)
Register a function to run as part of the unit test suite.