engine: complete C ABI facade (liboakengine oakengine_* surface)

The full pure-C facade used by the app: node/project/timeline/viewer/
undo/task/events/serializer/playback/preview/renderer/gizmo/color/
audio/footage/proxy/encoding/exporter/config/disk/ipc/plugin/worker
families, plus undo-group semantics, display renderer handles,
NodeFactory accessors, and per-family pure-C engine tests.
This commit is contained in:
2026-07-26 22:43:00 +08:00
parent c486c853ff
commit e9f173916f
80 changed files with 28715 additions and 126 deletions
+510
View File
@@ -0,0 +1,510 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the liboakengine application facade
// (oakengine/app.h). Exercises the CoreParams startup, the start/stop state
// machine, the tool/snapping/timecode state with its change notifications,
// the recent-projects list, the status bar, the clipboard, the footage
// filter and the project lifecycle. No GPU: everything runs on the
// offscreen QGuiApplication created by the facade itself.
//
// Not covered (they require a running import/load task or the autorecovery
// timer, which need an event loop): the confirm_image_sequence,
// relink_footage, save_project and load_layout handler invocations.
// Registration of every handler field is exercised and the close_project
// handler is verified through oakengine_app_create_new_project().
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "oakengine/app.h"
#include "oakengine/init.h"
#include "oakengine/project.h"
// Recording sink for every OakEngineAppCallbacks field.
typedef struct {
int confirm_image_sequence_calls;
int relink_calls;
int save_project_calls;
int close_project_calls;
int close_project_ret;
int load_layout_calls;
int otio_import_calls;
int status_show_calls;
char last_status[256];
int last_timeout;
int status_clear_calls;
int cache_full_calls;
int active_project_calls;
OakEngineProject *last_project;
int tool_changed_calls;
int last_tool;
int addable_changed_calls;
int last_addable;
int snapping_changed_calls;
int last_snapping;
int timecode_changed_calls;
int last_display;
int recent_changed_calls;
int color_picker_calls;
int last_color_picker;
} Cb;
static Cb g_cb;
static int on_confirm_image_sequence(const char *filename, void *userdata)
{
(void) filename;
assert(userdata == &g_cb);
g_cb.confirm_image_sequence_calls++;
return 1;
}
static int on_relink_footage(OakEngineFootage **footage, int count,
void *userdata)
{
(void) footage;
assert(userdata == &g_cb);
g_cb.relink_calls++;
assert(count >= 0);
return 1;
}
static void on_save_project(const char *override_filename, void *userdata)
{
(void) override_filename;
assert(userdata == &g_cb);
g_cb.save_project_calls++;
}
static int on_close_project(void *userdata)
{
assert(userdata == &g_cb);
g_cb.close_project_calls++;
// Mirror the application's close: detach and delete the open project
OakEngineProject *p = oakengine_app_open_project();
if (p) {
oakengine_app_set_active_project(NULL);
oakengine_project_free(p);
}
return g_cb.close_project_ret;
}
static void on_load_layout(const void *layout, void *userdata)
{
(void) layout;
assert(userdata == &g_cb);
g_cb.load_layout_calls++;
}
static int on_otio_import(OakEngineSequence **sequences, int count,
void *userdata)
{
(void) sequences;
(void) count;
assert(userdata == &g_cb);
g_cb.otio_import_calls++;
return 1;
}
static void on_status_message_show(const char *message, int timeout,
void *userdata)
{
assert(userdata == &g_cb);
g_cb.status_show_calls++;
snprintf(g_cb.last_status, sizeof(g_cb.last_status), "%s", message);
g_cb.last_timeout = timeout;
}
static void on_status_message_clear(void *userdata)
{
assert(userdata == &g_cb);
g_cb.status_clear_calls++;
}
static void on_cache_full_warning(void *userdata)
{
assert(userdata == &g_cb);
g_cb.cache_full_calls++;
}
static void on_active_project_changed(OakEngineProject *project,
void *userdata)
{
assert(userdata == &g_cb);
g_cb.active_project_calls++;
g_cb.last_project = project;
}
static void on_tool_changed(int tool, void *userdata)
{
assert(userdata == &g_cb);
g_cb.tool_changed_calls++;
g_cb.last_tool = tool;
}
static void on_addable_object_changed(int object, void *userdata)
{
assert(userdata == &g_cb);
g_cb.addable_changed_calls++;
g_cb.last_addable = object;
}
static void on_snapping_changed(int snapping, void *userdata)
{
assert(userdata == &g_cb);
g_cb.snapping_changed_calls++;
g_cb.last_snapping = snapping;
}
static void on_timecode_display_changed(int display, void *userdata)
{
assert(userdata == &g_cb);
g_cb.timecode_changed_calls++;
g_cb.last_display = display;
}
static void on_open_recent_list_changed(void *userdata)
{
assert(userdata == &g_cb);
g_cb.recent_changed_calls++;
}
static void on_color_picker_enabled(int enabled, void *userdata)
{
assert(userdata == &g_cb);
g_cb.color_picker_calls++;
g_cb.last_color_picker = enabled;
}
static OakEngineAppCallbacks make_callbacks(void)
{
OakEngineAppCallbacks cb = { 0 };
cb.userdata = &g_cb;
cb.confirm_image_sequence = on_confirm_image_sequence;
cb.relink_footage = on_relink_footage;
cb.save_project = on_save_project;
cb.close_project = on_close_project;
cb.load_layout = on_load_layout;
cb.otio_import = on_otio_import;
cb.status_message_show = on_status_message_show;
cb.status_message_clear = on_status_message_clear;
cb.cache_full_warning = on_cache_full_warning;
cb.active_project_changed = on_active_project_changed;
cb.tool_changed = on_tool_changed;
cb.addable_object_changed = on_addable_object_changed;
cb.snapping_changed = on_snapping_changed;
cb.timecode_display_changed = on_timecode_display_changed;
cb.open_recent_list_changed = on_open_recent_list_changed;
cb.color_picker_enabled = on_color_picker_enabled;
return cb;
}
// Query a buf/size string function into a heap buffer (caller frees).
static char *query0(int (*fn)(char *, int))
{
const int needed = fn(NULL, 0);
assert(needed >= 0);
char *buf = (char *) malloc(size_t(needed) + 1);
assert(fn(buf, needed + 1) == needed);
buf[needed] = '\0';
return buf;
}
static void test_create_and_params(void)
{
OakEngineAppParams params = { 0 };
params.run_mode = OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE;
params.fullscreen = 1;
params.startup_project = "/tmp/startup.ove";
// NULL params would be valid too, but verify the values round-trip
assert(oakengine_app_create(&params) == OAKENGINE_OK);
assert(oakengine_app_run_mode() == OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE);
assert(oakengine_app_fullscreen() == 1);
char *startup = query0(oakengine_app_startup_project);
assert(strcmp(startup, "/tmp/startup.ove") == 0);
free(startup);
// Only one application core may exist
assert(oakengine_app_create(NULL) == OAKENGINE_E_STATE);
}
static void test_start_stop(void)
{
// Not started yet
assert(oakengine_app_stop() == OAKENGINE_E_STATE);
// Full engine start (config, managers, autorecovery, recent list)
assert(oakengine_app_start() == OAKENGINE_OK);
assert(oakengine_app_start() == OAKENGINE_E_STATE);
assert(oakengine_app_stop() == OAKENGINE_OK);
assert(oakengine_app_stop() == OAKENGINE_E_STATE);
}
static void test_tool_state(void)
{
OakEngineAppCallbacks cb = make_callbacks();
assert(oakengine_app_set_callbacks(&cb) == OAKENGINE_OK);
// Tool (k_none=0 .. k_track_select=13, k_count=14)
assert(oakengine_app_set_tool(4) == OAKENGINE_OK);
assert(oakengine_app_tool() == 4);
assert(g_cb.tool_changed_calls == 1 && g_cb.last_tool == 4);
assert(oakengine_app_set_tool(-1) == OAKENGINE_E_INVALID);
assert(oakengine_app_set_tool(999) == OAKENGINE_E_INVALID);
// Addable object
assert(oakengine_app_set_addable_object(2) == OAKENGINE_OK);
assert(oakengine_app_addable_object() == 2);
assert(g_cb.addable_changed_calls == 1 && g_cb.last_addable == 2);
assert(oakengine_app_set_addable_object(999) == OAKENGINE_E_INVALID);
// Snapping
assert(oakengine_app_set_snapping(0) == OAKENGINE_OK);
assert(oakengine_app_snapping() == 0);
assert(g_cb.snapping_changed_calls == 1 && g_cb.last_snapping == 0);
assert(oakengine_app_set_snapping(1) == OAKENGINE_OK);
assert(oakengine_app_snapping() == 1);
assert(g_cb.snapping_changed_calls == 2 && g_cb.last_snapping == 1);
// Timecode display (0..4)
assert(oakengine_app_set_timecode_display(3) == OAKENGINE_OK);
assert(oakengine_app_timecode_display() == 3);
assert(g_cb.timecode_changed_calls == 1 && g_cb.last_display == 3);
assert(oakengine_app_set_timecode_display(999) == OAKENGINE_E_INVALID);
// Selected transition
assert(oakengine_app_set_selected_transition("crossdissolve") ==
OAKENGINE_OK);
char *transition = query0(oakengine_app_selected_transition);
assert(strcmp(transition, "crossdissolve") == 0);
free(transition);
assert(oakengine_app_set_selected_transition(NULL) == OAKENGINE_OK);
transition = query0(oakengine_app_selected_transition);
assert(transition[0] == '\0');
free(transition);
// Magic flag
assert(oakengine_app_set_magic(1) == OAKENGINE_OK);
assert(oakengine_app_is_magic_enabled() == 1);
assert(oakengine_app_set_magic(0) == OAKENGINE_OK);
assert(oakengine_app_is_magic_enabled() == 0);
}
static void test_status_and_pixel_sampling(void)
{
assert(oakengine_app_show_status_message("hello", 250) == OAKENGINE_OK);
assert(g_cb.status_show_calls == 1);
assert(strcmp(g_cb.last_status, "hello") == 0);
assert(g_cb.last_timeout == 250);
assert(oakengine_app_show_status_message(NULL, 0) == OAKENGINE_E_INVALID);
assert(oakengine_app_clear_status_message() == OAKENGINE_OK);
assert(g_cb.status_clear_calls == 1);
// Pixel sampling ref-count emits only when crossing zero
assert(oakengine_app_request_pixel_sampling(1) == OAKENGINE_OK);
assert(oakengine_app_request_pixel_sampling(1) == OAKENGINE_OK);
assert(g_cb.color_picker_calls == 1 && g_cb.last_color_picker == 1);
assert(oakengine_app_request_pixel_sampling(0) == OAKENGINE_OK);
assert(g_cb.color_picker_calls == 1);
assert(oakengine_app_request_pixel_sampling(0) == OAKENGINE_OK);
assert(g_cb.color_picker_calls == 2 && g_cb.last_color_picker == 0);
}
static void test_project_lifecycle(void)
{
g_cb.close_project_ret = 1;
const int active_before = g_cb.active_project_calls;
// New project goes through the close handler and becomes active
assert(oakengine_app_create_new_project() == OAKENGINE_OK);
assert(g_cb.close_project_calls == 1);
OakEngineProject *p = oakengine_app_open_project();
assert(p != NULL);
assert(g_cb.active_project_calls > active_before);
assert(g_cb.last_project == p);
// Replacing it invokes the close handler again
assert(oakengine_app_create_new_project() == OAKENGINE_OK);
assert(g_cb.close_project_calls == 2);
p = oakengine_app_open_project();
assert(p != NULL);
// Saving a project without a filename keeps the recent list unchanged
assert(oakengine_app_clear_recent_projects() == OAKENGINE_OK);
assert(oakengine_app_on_project_saved(p) == OAKENGINE_OK);
assert(oakengine_app_recent_projects_count() == 0);
// Detach and free it again
assert(oakengine_app_set_active_project(NULL) == OAKENGINE_OK);
assert(oakengine_app_open_project() == NULL);
assert(g_cb.last_project == NULL);
oakengine_project_free(p);
// add_open_project adopts an externally created project
p = oakengine_project_create();
assert(p != NULL);
assert(oakengine_project_new(p) == OAKENGINE_OK);
assert(oakengine_app_add_open_project(p, 0) == OAKENGINE_OK);
assert(oakengine_app_open_project() == p);
assert(oakengine_app_set_active_project(NULL) == OAKENGINE_OK);
oakengine_project_free(p);
// NULL tolerance
assert(oakengine_app_add_open_project(NULL, 0) == OAKENGINE_E_INVALID);
assert(oakengine_app_on_project_saved(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_app_add_open_project_from_task(NULL, 0) ==
OAKENGINE_E_INVALID);
assert(oakengine_app_add_recovery_project_from_task(NULL) ==
OAKENGINE_E_INVALID);
}
static void test_recent_projects(void)
{
// Start from a clean list
assert(oakengine_app_clear_recent_projects() == OAKENGINE_OK);
assert(oakengine_app_recent_projects_count() == 0);
const int changes_before = g_cb.recent_changed_calls;
// A saved project lands in the recent list through on_project_saved
OakEngineProject *p = oakengine_project_create();
assert(p != NULL);
assert(oakengine_project_new(p) == OAKENGINE_OK);
assert(oakengine_project_save(p, "oakengine_app_test_recent.ove") ==
OAKENGINE_OK);
assert(oakengine_app_on_project_saved(p) == OAKENGINE_OK);
assert(oakengine_app_recent_projects_count() == 1);
assert(g_cb.recent_changed_calls > changes_before);
const int needed = oakengine_app_recent_project_at(0, NULL, 0);
assert(needed > 0);
char *buf = (char *) malloc(size_t(needed) + 1);
assert(oakengine_app_recent_project_at(0, buf, needed + 1) == needed);
buf[needed] = '\0';
assert(strstr(buf, "oakengine_app_test_recent.ove") != NULL);
free(buf);
// Out-of-range access is rejected (the engine would assert otherwise)
assert(oakengine_app_recent_project_at(5, NULL, 0) ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_app_remove_recent_project(5) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_app_remove_recent_project(0) == OAKENGINE_OK);
assert(oakengine_app_recent_projects_count() == 0);
remove("oakengine_app_test_recent.ove");
oakengine_project_free(p);
}
static void test_clipboard(void)
{
assert(oakengine_app_copy_to_clipboard("hello clipboard") ==
OAKENGINE_OK);
char *text = query0(oakengine_app_paste_from_clipboard);
assert(strcmp(text, "hello clipboard") == 0);
free(text);
assert(oakengine_app_copy_to_clipboard(NULL) == OAKENGINE_E_INVALID);
}
static void test_footage_filter(void)
{
assert(oakengine_app_is_footage_extension_allowed("movie.mp4") == 1);
assert(oakengine_app_is_footage_extension_allowed("IMAGE.PNG") == 1);
assert(oakengine_app_is_footage_extension_allowed("doc.txt") == 0);
assert(oakengine_app_is_footage_extension_allowed(NULL) ==
OAKENGINE_E_INVALID);
char *filter = query0(oakengine_app_footage_file_dialog_filter);
assert(strstr(filter, "*.mp4") != NULL);
assert(strstr(filter, ";;") != NULL);
free(filter);
}
static void test_misc(void)
{
// Unknown locale is reported as "not found" without failing
assert(oakengine_app_set_language("definitely_not_a_locale_xx") == 0);
assert(oakengine_app_set_language(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_app_set_autorecovery_interval(5) == OAKENGINE_OK);
assert(oakengine_app_set_use_proxy_media(1) == OAKENGINE_OK);
char *index = query0(oakengine_app_auto_recovery_index_filename);
assert(strstr(index, "unrecovered") != NULL);
free(index);
assert(oakengine_app_undo_stack() != NULL);
}
static void test_create_sequence(void)
{
OakEngineProject *p = oakengine_project_create();
assert(p != NULL);
assert(oakengine_project_new(p) == OAKENGINE_OK);
OakEngineSequence *s = oakengine_app_create_sequence(p, "Seq %1");
assert(s != NULL);
assert(oakengine_app_create_sequence(NULL, NULL) == NULL);
// The returned sequence is not yet part of the project (owned by the
// caller); this test intentionally leaves it unparented.
oakengine_project_free(p);
}
static void test_callbacks_clear(void)
{
assert(oakengine_app_set_callbacks(NULL) == OAKENGINE_OK);
// State changes still work, but nothing is delivered anymore
const int calls = g_cb.tool_changed_calls;
assert(oakengine_app_set_tool(2) == OAKENGINE_OK);
assert(oakengine_app_tool() == 2);
assert(g_cb.tool_changed_calls == calls);
}
int main(void)
{
// The facade brings up its own offscreen application object
test_create_and_params();
test_start_stop();
// Engine services (incl. renderer manager for set_active_project)
assert(oakengine_init(OAKENGINE_INIT_HEADLESS | OAKENGINE_INIT_RENDER) ==
OAKENGINE_OK);
test_tool_state();
test_status_and_pixel_sampling();
test_project_lifecycle();
test_recent_projects();
test_clipboard();
test_footage_filter();
test_misc();
test_create_sequence();
test_callbacks_clear();
assert(oakengine_shutdown() == OAKENGINE_OK);
return 0;
}
+203
View File
@@ -0,0 +1,203 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the liboakengine audio I/O family
// (oakengine/audio.h). Exercises the AudioManager instance lifecycle,
// input/output device get/set round-trips, output push error paths and the
// output_params_changed event subscription. No GL or QApplication required.
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "oakengine/audio.h"
#include "oakengine/events.h"
#include "oakengine/init.h"
static int g_output_params_changed_count;
static void *g_output_params_changed_source;
static void on_output_params_changed(const oakengine_event *event, void *)
{
assert(event != NULL);
assert(event->id == OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED);
g_output_params_changed_count++;
g_output_params_changed_source = event->source;
}
static void test_instance_lifecycle(void)
{
// No instance before create.
assert(oakengine_audio_manager_handle() == NULL);
assert(oakengine_audio_get_output_device() == -1);
assert(oakengine_audio_get_input_device() == -1);
assert(oakengine_audio_clear_buffered_output() == OAKENGINE_E_STATE);
assert(oakengine_audio_stop_recording() == OAKENGINE_E_STATE);
assert(oakengine_audio_create_instance() == OAKENGINE_OK);
assert(oakengine_audio_manager_handle() != NULL);
// Idempotent create is allowed.
assert(oakengine_audio_create_instance() == OAKENGINE_OK);
assert(oakengine_audio_manager_handle() != NULL);
oakengine_audio_destroy_instance();
assert(oakengine_audio_manager_handle() == NULL);
// Idempotent destroy is allowed.
assert(oakengine_audio_destroy_instance() == OAKENGINE_OK);
assert(oakengine_audio_manager_handle() == NULL);
}
static void test_device_round_trip(void)
{
assert(oakengine_audio_create_instance() == OAKENGINE_OK);
void *handle = oakengine_audio_manager_handle();
assert(handle != NULL);
// Default is usually paNoDevice (-1) in headless environments.
const int64_t original_output = oakengine_audio_get_output_device();
const int64_t original_input = oakengine_audio_get_input_device();
// Setting a value should change the returned value.
assert(oakengine_audio_set_output_device(42) == OAKENGINE_OK);
assert(oakengine_audio_get_output_device() == 42);
assert(oakengine_audio_set_input_device(43) == OAKENGINE_OK);
assert(oakengine_audio_get_input_device() == 43);
// hard_reset re-initializes PortAudio and should not crash.
assert(oakengine_audio_hard_reset() == OAKENGINE_OK);
assert(oakengine_audio_manager_handle() == handle);
// Restore original values.
assert(oakengine_audio_set_output_device(original_output) == OAKENGINE_OK);
assert(oakengine_audio_set_input_device(original_input) == OAKENGINE_OK);
assert(oakengine_audio_get_output_device() == original_output);
assert(oakengine_audio_get_input_device() == original_input);
oakengine_audio_destroy_instance();
}
static void test_push_to_output_errors(void)
{
assert(oakengine_audio_create_instance() == OAKENGINE_OK);
char error_buf[256];
memset(error_buf, 0, sizeof(error_buf));
// NULL params is rejected without crashing.
assert(oakengine_audio_push_to_output(NULL, "x", 1, error_buf,
sizeof(error_buf)) ==
OAKENGINE_E_INVALID);
// NULL samples is rejected.
assert(oakengine_audio_push_to_output((const OakAudioParams *)1, NULL, 1,
error_buf, sizeof(error_buf)) ==
OAKENGINE_E_INVALID);
oakengine_audio_destroy_instance();
}
static void test_output_params_changed_event(void)
{
assert(oakengine_audio_create_instance() == OAKENGINE_OK);
void *handle = oakengine_audio_manager_handle();
assert(handle != NULL);
g_output_params_changed_count = 0;
g_output_params_changed_source = NULL;
const int64_t sub = oakengine_event_subscribe(
handle, OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED,
on_output_params_changed, NULL);
assert(sub > 0);
const int64_t original_output = oakengine_audio_get_output_device();
assert(oakengine_audio_set_output_device(84) == OAKENGINE_OK);
assert(g_output_params_changed_count >= 1);
assert(g_output_params_changed_source == handle);
assert(oakengine_event_unsubscribe(sub) == OAKENGINE_OK);
// No further deliveries after unsubscribe.
const int count_after_unsub = g_output_params_changed_count;
assert(oakengine_audio_set_output_device(85) == OAKENGINE_OK);
assert(g_output_params_changed_count == count_after_unsub);
// Restore original value.
assert(oakengine_audio_set_output_device(original_output) == OAKENGINE_OK);
oakengine_audio_destroy_instance();
}
static void test_audio_sync_algorithms(void)
{
// place_by_waveform_offset: a 1-second positive offset at 48 kHz
oak_audio_sync_placement placement;
assert(oakengine_audio_sync_place_by_waveform_offset(
0, 1, 48000, 48000, &placement) == OAKENGINE_OK);
assert(placement.valid);
assert(placement.timeline_in_num == 1 && placement.timeline_in_den == 1);
// place_by_source_time: matching source/media in points -> same timeline in
oak_audio_sync_source_clip ref = { 0, 1, 0, 1, 1 };
oak_audio_sync_source_clip cand = { 0, 1, 0, 1, 1 };
assert(oakengine_audio_sync_place_by_source_time(
&ref, &cand, 5, 1, &placement) == OAKENGINE_OK);
assert(placement.valid);
assert(placement.timeline_in_num == 5 && placement.timeline_in_den == 1);
// estimate_envelope_offset: identical envelopes -> zero offset, high
// confidence
double envelope[10] = { 0, 1, 2, 3, 4, 5, 4, 3, 2, 1 };
oak_audio_waveform_offset offset;
assert(oakengine_audio_estimate_envelope_offset(
envelope, 10, envelope, 10, NULL, 0, NULL, 0, 1, 5,
&offset) == OAKENGINE_OK);
assert(offset.valid);
assert(offset.offset_samples == 0);
assert(offset.confidence > 0.99);
// estimate_stretch_and_offset: identical envelopes at rate 1 -> zero offset
oak_audio_waveform_stretch_offset stretch;
assert(oakengine_audio_estimate_stretch_and_offset(
envelope, 10, envelope, 10, NULL, 0, NULL, 0, 1, 5, 0.9, 1.1,
0.05, &stretch) == OAKENGINE_OK);
assert(stretch.valid);
assert(stretch.offset_samples == 0);
assert(stretch.rate > 0.99 && stretch.rate < 1.01);
}
int main(void)
{
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
test_instance_lifecycle();
test_device_round_trip();
test_push_to_output_errors();
test_output_params_changed_event();
test_audio_sync_algorithms();
oakengine_shutdown();
printf("oakengine_audio_test: OK\n");
return 0;
}
+432
View File
@@ -0,0 +1,432 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the liboakengine color facade (oakengine/color.h)
// and the color manager events (oakengine/events.h). Exercises the
// manager queries (config filename, colorspace/display/view/look lists,
// defaults, luma coefficients, compliant-space resolution), the standalone
// config handle, the color processor handle (create/convert/id) and the
// event subscriptions. No GPU: everything here runs on the CPU-side OCIO
// wrappers. When the environment provides no usable OCIO config at all
// (colorspace count 0), the query assertions are skipped but the
// robustness checks (NULL handling, error paths) still run.
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
#include <direct.h>
#include <windows.h>
#else
#include <unistd.h>
#endif
#include "oakengine/color.h"
#include "oakengine/events.h"
#include "oakengine/init.h"
#include "oakengine/project.h"
static char g_tmpdir[4096];
static void make_tmpdir(void)
{
#if defined(_WIN32)
char base[MAX_PATH];
const DWORD len = GetTempPathA(MAX_PATH, base);
assert(len > 0 && len < MAX_PATH);
snprintf(g_tmpdir, sizeof(g_tmpdir), "%soakengine_color_test_%lu", base,
(unsigned long)GetCurrentProcessId());
assert(_mkdir(g_tmpdir) == 0);
#else
strcpy(g_tmpdir, "/tmp/oakengine_color_test_XXXXXX");
assert(mkdtemp(g_tmpdir) != NULL);
#endif
}
// ---- Robustness: NULL/invalid arguments ------------------------------------
static void test_null_robustness(void)
{
char buf[64];
double rgb[3];
double rgba[4] = { 0, 0, 0, 0 };
assert(oakengine_color_manager_from_project(NULL) == NULL);
assert(oakengine_color_manager_get_config_filename(NULL, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_set_config_filename(NULL, "x") ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_colorspace_count(NULL) == 0);
assert(oakengine_color_manager_colorspace_at(NULL, 0, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_display_count(NULL) == 0);
assert(oakengine_color_manager_display_at(NULL, 0, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_view_count(NULL, NULL) == 0);
assert(oakengine_color_manager_view_at(NULL, NULL, 0, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_look_count(NULL) == 0);
assert(oakengine_color_manager_look_at(NULL, 0, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_default_display(NULL, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_default_view(NULL, NULL, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_default_input_color_space(NULL, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_set_default_input_color_space(NULL, "x") ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_reference_color_space(NULL, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_default_luma_coefs(NULL, rgb) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_compliant_color_space(NULL, "x", buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_manager_compliant_transform(NULL, NULL, 0, NULL,
NULL, 0, NULL, 0, NULL,
0) == OAKENGINE_E_INVALID);
assert(oakengine_color_config_load_file(NULL) == NULL);
assert(oakengine_color_config_colorspace_count(NULL) == 0);
assert(oakengine_color_config_colorspace_at(NULL, 0, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
oakengine_color_config_free(NULL); // no-op
assert(oakengine_color_processor_create(NULL, "in", NULL,
OAKENGINE_COLOR_PROCESSOR_NORMAL) ==
NULL);
assert(oakengine_color_processor_is_valid(NULL) == 0);
assert(oakengine_color_processor_convert_color(NULL, rgba, rgba) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_processor_id(NULL, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
oakengine_color_processor_free(NULL); // no-op
}
// ---- Standalone config handle -----------------------------------------------
static void test_config_handle(int have_ocio)
{
char buf[256];
// A missing file must fail cleanly with an error message.
assert(oakengine_color_config_load_file("/nonexistent/definitely.ocio") ==
NULL);
assert(oakengine_color_last_error(buf, sizeof(buf)) > 0);
OakEngineColorConfig *config = oakengine_color_config_load_default();
if (!have_ocio) {
// No usable OCIO config in this environment.
if (config) {
oakengine_color_config_free(config);
}
return;
}
assert(config != NULL);
const int count = oakengine_color_config_colorspace_count(config);
assert(count > 0);
for (int i = 0; i < count; i++) {
assert(oakengine_color_config_colorspace_at(config, i, buf,
sizeof(buf)) > 0);
assert(buf[0] != '\0');
}
assert(oakengine_color_config_colorspace_at(config, count, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_color_config_colorspace_at(config, -1, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
oakengine_color_config_free(config);
}
// ---- Manager queries ---------------------------------------------------------
static void test_manager_queries(OakEngineColorManager *mgr)
{
char buf[256];
char first_cs[256];
double rgb[3] = { 0, 0, 0 };
// Colorspaces
const int cs_count = oakengine_color_manager_colorspace_count(mgr);
assert(cs_count > 0);
assert(oakengine_color_manager_colorspace_at(mgr, 0, first_cs,
sizeof(first_cs)) > 0);
assert(first_cs[0] != '\0');
assert(oakengine_color_manager_colorspace_at(mgr, cs_count, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
// Displays / views / looks
const int disp_count = oakengine_color_manager_display_count(mgr);
assert(disp_count > 0);
assert(oakengine_color_manager_display_at(mgr, 0, buf, sizeof(buf)) > 0);
char display[256];
memcpy(display, buf, sizeof(display));
const int view_count = oakengine_color_manager_view_count(mgr, display);
assert(view_count > 0);
assert(oakengine_color_manager_view_at(mgr, display, 0, buf, sizeof(buf)) >
0);
assert(oakengine_color_manager_look_count(mgr) >= 0);
assert(oakengine_color_manager_display_at(mgr, disp_count, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
// Defaults
char default_display[256];
assert(oakengine_color_manager_default_display(mgr, default_display,
sizeof(default_display)) > 0);
assert(oakengine_color_manager_default_view(mgr, default_display, buf,
sizeof(buf)) > 0);
assert(oakengine_color_manager_default_input_color_space(mgr, buf,
sizeof(buf)) > 0);
assert(oakengine_color_manager_reference_color_space(mgr, buf,
sizeof(buf)) > 0);
// Default input colorspace set/get roundtrip
assert(oakengine_color_manager_set_default_input_color_space(mgr,
first_cs) ==
OAKENGINE_OK);
assert(oakengine_color_manager_default_input_color_space(mgr, buf,
sizeof(buf)) > 0);
assert(strcmp(buf, first_cs) == 0);
// Config filename set/get roundtrip (an empty filename selects the
// built-in default config; setting it must not crash the queries above)
assert(oakengine_color_manager_set_config_filename(mgr, "") ==
OAKENGINE_OK);
assert(oakengine_color_manager_get_config_filename(mgr, buf, sizeof(buf)) >=
0);
// Luma coefficients: Rec.709-style weights, all positive, roughly sum to 1
assert(oakengine_color_manager_default_luma_coefs(mgr, rgb) ==
OAKENGINE_OK);
assert(rgb[0] > 0 && rgb[1] > 0 && rgb[2] > 0);
assert(fabs(rgb[0] + rgb[1] + rgb[2] - 1.0) < 0.01);
// Compliant colorspace: an existing space resolves to itself, an empty
// name resolves to the default input space
assert(oakengine_color_manager_compliant_color_space(mgr, first_cs, buf,
sizeof(buf)) > 0);
assert(strcmp(buf, first_cs) == 0);
assert(oakengine_color_manager_compliant_color_space(mgr, "", buf,
sizeof(buf)) > 0);
assert(strcmp(buf, first_cs) == 0);
// Compliant transform: force a colorspace transform onto a display
// transform and back
oak_color_transform in;
in.is_display = 0;
in.output = first_cs;
in.view = NULL;
in.look = NULL;
int out_is_display = -1;
char out_o[256], out_v[256], out_l[256];
out_o[0] = out_v[0] = out_l[0] = '\0';
assert(oakengine_color_manager_compliant_transform(mgr, &in, 1,
&out_is_display, out_o,
sizeof(out_o), out_v,
sizeof(out_v), out_l,
sizeof(out_l)) ==
OAKENGINE_OK);
assert(out_is_display == 1);
assert(out_o[0] != '\0');
assert(oakengine_color_manager_compliant_transform(
mgr, &in, 0, &out_is_display, out_o, sizeof(out_o), out_v,
sizeof(out_v), out_l, sizeof(out_l)) == OAKENGINE_OK);
assert(out_is_display == 0);
assert(strcmp(out_o, first_cs) == 0);
}
// ---- Color processor ----------------------------------------------------------
static void test_processor(OakEngineColorManager *mgr)
{
char ref[256];
char buf[256];
assert(oakengine_color_manager_reference_color_space(mgr, ref,
sizeof(ref)) > 0);
// Identity transform (ref -> ref): white stays white
oak_color_transform dest;
dest.is_display = 0;
dest.output = ref;
dest.view = NULL;
dest.look = NULL;
OakEngineColorProcessor *proc = oakengine_color_processor_create(
mgr, ref, &dest, OAKENGINE_COLOR_PROCESSOR_NORMAL);
assert(proc != NULL);
assert(oakengine_color_processor_is_valid(proc) == 1);
const double in[4] = { 1.0, 1.0, 1.0, 1.0 };
double out[4] = { 0, 0, 0, 0 };
assert(oakengine_color_processor_convert_color(proc, in, out) ==
OAKENGINE_OK);
assert(fabs(out[0] - 1.0) < 1e-3 && fabs(out[1] - 1.0) < 1e-3 &&
fabs(out[2] - 1.0) < 1e-3 && fabs(out[3] - 1.0) < 1e-3);
// Cache id is non-empty and stable
const int id_len = oakengine_color_processor_id(proc, buf, sizeof(buf));
assert(id_len > 0);
assert(buf[0] != '\0');
assert(oakengine_color_processor_id(proc, NULL, 0) == id_len);
// Inverse direction constructs too
OakEngineColorProcessor *inv = oakengine_color_processor_create(
mgr, ref, &dest, OAKENGINE_COLOR_PROCESSOR_INVERSE);
assert(inv != NULL);
oakengine_color_processor_free(inv);
// Unknown direction is rejected
assert(oakengine_color_processor_create(mgr, ref, &dest, 7) == NULL);
// An unknown colorspace yields an invalid (pass-through) processor,
// matching the engine's non-throwing C++ behavior
dest.output = "definitely-not-a-colorspace";
OakEngineColorProcessor *bad = oakengine_color_processor_create(
mgr, ref, &dest, OAKENGINE_COLOR_PROCESSOR_NORMAL);
assert(bad != NULL);
if (oakengine_color_processor_is_valid(bad)) {
// Some configs resolve unknown names via roles; then conversion must
// still not crash.
assert(oakengine_color_processor_convert_color(bad, in, out) ==
OAKENGINE_OK);
} else {
out[0] = out[1] = out[2] = out[3] = 0;
assert(oakengine_color_processor_convert_color(bad, in, out) ==
OAKENGINE_OK);
assert(out[0] == 1.0 && out[1] == 1.0 && out[2] == 1.0 &&
out[3] == 1.0);
}
oakengine_color_processor_free(bad);
// Processor is valid and usable.
assert(proc != NULL);
oakengine_color_processor_free(proc);
}
// ---- Events --------------------------------------------------------------------
static int g_config_events = 0;
static int g_reference_events = 0;
static void count_color_events(const oakengine_event *event, void *userdata)
{
(void)userdata;
assert(event != NULL);
if (event->id == OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED) {
g_config_events++;
} else if (event->id == OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED) {
g_reference_events++;
} else {
assert(0); // unexpected event id on this subscription
}
}
static void test_events(OakEngineProject *project,
OakEngineColorManager *mgr)
{
// Family mismatch: a color manager event on a project handle must fail.
assert(oakengine_event_subscribe(
project, OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED,
count_color_events, NULL) == 0);
int64_t sub_ref = oakengine_event_subscribe(
mgr, OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED,
count_color_events, NULL);
int64_t sub_cfg = oakengine_event_subscribe(
mgr, OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED,
count_color_events, NULL);
assert(sub_ref > 0);
assert(sub_cfg > 0);
// Changing the reference space emits reference_space_changed.
char ref[256];
assert(oakengine_project_get_color_reference_space(project, ref,
sizeof(ref)) > 0);
assert(oakengine_project_set_color_reference_space(
project, strcmp(ref, "scene_linear") == 0 ? "reference" :
"scene_linear") ==
OAKENGINE_OK);
assert(g_reference_events == 1);
assert(g_config_events == 0);
assert(oakengine_event_unsubscribe(sub_ref) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_cfg) == OAKENGINE_OK);
}
int main(void)
{
make_tmpdir();
// Sandbox the config/cache/data locations (the default OCIO config is
// extracted under the cache location).
#if !defined(_WIN32)
assert(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0);
assert(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0);
assert(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0);
#endif
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
test_null_robustness();
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineColorManager *mgr = oakengine_color_manager_from_project(project);
assert(mgr != NULL);
// The built-in default config should always be available (it is
// extracted from the engine's resources); tolerate environments where
// it is not by skipping the query assertions.
const int have_ocio = oakengine_color_manager_colorspace_count(mgr) > 0;
if (!have_ocio) {
printf("oakengine_color_test: no OCIO config available, skipping "
"query tests\n");
}
test_config_handle(have_ocio);
if (have_ocio) {
test_manager_queries(mgr);
test_processor(mgr);
}
test_events(project, mgr);
oakengine_project_free(project);
assert(oakengine_shutdown() == OAKENGINE_OK);
printf("oakengine_color_test: all assertions passed\n");
return 0;
}
+118
View File
@@ -0,0 +1,118 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI tests for the liboakengine configuration facade
// (oakengine/config.h). Runs headless; no GPU required.
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "oakengine/config.h"
#include "oakengine/init.h"
static int g_error_calls = 0;
static char g_last_title[256];
static char g_last_message[256];
static void error_cb(const char *title, const char *message, void *userdata)
{
(void) userdata;
g_error_calls++;
strncpy(g_last_title, title, sizeof(g_last_title) - 1);
g_last_title[sizeof(g_last_title) - 1] = '\0';
strncpy(g_last_message, message, sizeof(g_last_message) - 1);
g_last_message[sizeof(g_last_message) - 1] = '\0';
}
static void test_string_round_trip(void)
{
char buf[256];
// Missing key returns 0 (empty string).
assert(oakengine_config_get_string("oak_test_string_key", buf,
sizeof(buf)) == 0);
assert(oakengine_config_set_string("oak_test_string_key",
"hello world") == OAKENGINE_OK);
int len = oakengine_config_get_string("oak_test_string_key", buf,
sizeof(buf));
assert(len == int(strlen("hello world")));
assert(strcmp(buf, "hello world") == 0);
// Query length with NULL buffer.
assert(oakengine_config_get_string("oak_test_string_key", NULL, 0) == len);
// NULL key is rejected.
assert(oakengine_config_get_string(NULL, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_config_set_string(NULL, "x") == OAKENGINE_E_INVALID);
}
static void test_int_round_trip(void)
{
assert(oakengine_config_get_int("oak_test_int_key", 42) == 42);
assert(oakengine_config_set_int("oak_test_int_key", 12345) ==
OAKENGINE_OK);
assert(oakengine_config_get_int("oak_test_int_key", 0) == 12345);
assert(oakengine_config_set_int("oak_test_int_key", -7) ==
OAKENGINE_OK);
assert(oakengine_config_get_int("oak_test_int_key", 0) == -7);
// NULL key returns default.
assert(oakengine_config_get_int(NULL, 99) == 99);
assert(oakengine_config_set_int(NULL, 1) == OAKENGINE_E_INVALID);
}
static void test_error_handler(void)
{
g_error_calls = 0;
assert(oakengine_config_set_error_handler(error_cb, NULL) ==
OAKENGINE_OK);
assert(oakengine_config_report_error("Test Title",
"Test Message") == OAKENGINE_OK);
assert(g_error_calls == 1);
assert(strcmp(g_last_title, "Test Title") == 0);
assert(strcmp(g_last_message, "Test Message") == 0);
// Clearing the handler does not crash.
assert(oakengine_config_set_error_handler(NULL, NULL) == OAKENGINE_OK);
assert(oakengine_config_report_error("Ignored", "Ignored") ==
OAKENGINE_OK);
assert(g_error_calls == 1);
}
int main(void)
{
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
test_string_round_trip();
test_int_round_trip();
test_error_handler();
assert(oakengine_config_save() == OAKENGINE_OK);
assert(oakengine_config_load() == OAKENGINE_OK);
assert(oakengine_shutdown() == OAKENGINE_OK);
return 0;
}
+301
View File
@@ -0,0 +1,301 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the liboakengine disk cache family
// (oakengine/disk.h). Exercises the DiskManager instance lifecycle, default
// cache path queries, cache clearing, settings handler dispatch, folder
// handle lookup and default path mutation. Runs headless; no GPU required.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
#include <direct.h>
#include <io.h>
#else
#include <sys/stat.h>
#include <unistd.h>
#endif
#include "oakengine/disk.h"
#include "oakengine/init.h"
static char g_handler_path[512];
static int g_handler_call_count;
static void reset_handler_state(void)
{
memset(g_handler_path, 0, sizeof(g_handler_path));
g_handler_call_count = 0;
}
static void settings_handler(const char *folder_path, void *parent_window,
void *userdata)
{
(void) parent_window;
(void) userdata;
assert(folder_path != NULL);
assert(strlen(folder_path) > 0);
strncpy(g_handler_path, folder_path, sizeof(g_handler_path) - 1);
g_handler_path[sizeof(g_handler_path) - 1] = '\0';
g_handler_call_count++;
}
static void test_instance_lifecycle(void)
{
char buf[512];
// DiskManager is created by oakengine_init(HEADLESS).
assert(oakengine_disk_get_default_cache_path(buf, sizeof(buf)) > 0);
// Destroy is allowed and idempotent.
assert(oakengine_disk_destroy_instance() == OAKENGINE_OK);
assert(oakengine_disk_get_default_cache_path(buf, sizeof(buf)) ==
OAKENGINE_E_STATE);
assert(oakengine_disk_destroy_instance() == OAKENGINE_OK);
// Create recreates the instance.
assert(oakengine_disk_create_instance() == OAKENGINE_OK);
assert(oakengine_disk_get_default_cache_path(buf, sizeof(buf)) > 0);
// Create is idempotent.
assert(oakengine_disk_create_instance() == OAKENGINE_OK);
assert(oakengine_disk_get_default_cache_path(buf, sizeof(buf)) > 0);
}
static void test_default_cache_path(void)
{
char buf[512];
memset(buf, 0, sizeof(buf));
const int len = oakengine_disk_get_default_cache_path(buf, sizeof(buf));
assert(len > 0);
assert((int) strlen(buf) == len);
assert(strchr(buf, '/') != NULL || strchr(buf, '\\') != NULL);
// Query length with NULL buffer.
assert(oakengine_disk_get_default_cache_path(NULL, 0) == len);
}
static void test_open_folder_handle(void)
{
char buf[512];
assert(oakengine_disk_get_default_cache_path(buf, sizeof(buf)) > 0);
void *folder = oakengine_disk_get_open_folder(buf);
assert(folder != NULL);
// NULL/empty path returns the default folder handle.
void *default_folder = oakengine_disk_get_open_folder(nullptr);
assert(default_folder == folder);
void *empty_folder = oakengine_disk_get_open_folder("");
assert(empty_folder == folder);
// A different path opens a distinct folder.
char tmp[256];
snprintf(tmp, sizeof(tmp),
#if defined(_WIN32)
"%s\\oakengine_disk_test_folder_XXXXXX",
#else
"%s/oakengine_disk_test_folder_XXXXXX",
#endif
getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp");
#if defined(_WIN32)
char *tmpdir = _mktemp(tmp);
assert(tmpdir != NULL);
assert(_mkdir(tmpdir) == 0);
#else
char *tmpdir = mkdtemp(tmp);
assert(tmpdir != NULL);
#endif
void *other_folder = oakengine_disk_get_open_folder(tmpdir);
assert(other_folder != NULL);
assert(other_folder != folder);
// The same path returns the same handle.
void *other_folder_again = oakengine_disk_get_open_folder(tmpdir);
assert(other_folder_again == other_folder);
#if defined(_WIN32)
_rmdir(tmpdir);
#else
rmdir(tmpdir);
#endif
}
static void test_clear_cache(void)
{
// Create a temporary cache directory and seed it with a file.
char path[256];
snprintf(path, sizeof(path),
#if defined(_WIN32)
"%s\\oakengine_disk_test_cache_XXXXXX",
#else
"%s/oakengine_disk_test_cache_XXXXXX",
#endif
getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp");
#if defined(_WIN32)
char *tmpdir = _mktemp(path);
assert(tmpdir != NULL);
assert(_mkdir(tmpdir) == 0);
#else
char *tmpdir = mkdtemp(path);
assert(tmpdir != NULL);
#endif
char index_file[512];
snprintf(index_file, sizeof(index_file),
#if defined(_WIN32)
"%s\\index", tmpdir);
#else
"%s/index", tmpdir);
#endif
FILE *f = fopen(index_file, "w");
assert(f != NULL);
fclose(f);
// clear_cache opens the folder and clears its contents.
assert(oakengine_disk_clear_cache(tmpdir) == 1);
// Re-create a file and clear again to ensure idempotency.
f = fopen(index_file, "w");
assert(f != NULL);
fclose(f);
assert(oakengine_disk_clear_cache(tmpdir) == 1);
#if defined(_WIN32)
_rmdir(tmpdir);
#else
rmdir(tmpdir);
#endif
}
static void test_settings_handler_round_trip(void)
{
reset_handler_state();
assert(oakengine_disk_set_settings_handler(settings_handler, NULL) ==
OAKENGINE_OK);
// NULL path uses the default folder.
assert(oakengine_disk_show_settings_dialog(NULL, NULL) == OAKENGINE_OK);
assert(g_handler_call_count == 1);
assert(strlen(g_handler_path) > 0);
// Calling again with a specific path invokes the handler with that path.
assert(oakengine_disk_show_settings_dialog(g_handler_path, NULL) ==
OAKENGINE_OK);
assert(g_handler_call_count == 2);
assert(strcmp(g_handler_path, g_handler_path) == 0);
// Clearing the handler is allowed and results in a logged skip.
assert(oakengine_disk_set_settings_handler(NULL, NULL) == OAKENGINE_OK);
assert(oakengine_disk_show_settings_dialog(g_handler_path, NULL) ==
OAKENGINE_OK);
assert(g_handler_call_count == 2);
}
static void test_invalidate_project(void)
{
// No instance returns an error.
assert(oakengine_disk_destroy_instance() == OAKENGINE_OK);
assert(oakengine_disk_invalidate_project(NULL) == OAKENGINE_E_STATE);
assert(oakengine_disk_create_instance() == OAKENGINE_OK);
// NULL project is accepted (signal emitted with null pointer).
assert(oakengine_disk_invalidate_project(NULL) == OAKENGINE_OK);
// Valid project returns OK without crashing.
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_disk_invalidate_project(project) == OAKENGINE_OK);
oakengine_project_free(project);
}
static void test_set_default_cache_path(void)
{
char original[512];
assert(oakengine_disk_get_default_cache_path(original, sizeof(original)) >
0);
char tmp[256];
snprintf(tmp, sizeof(tmp),
#if defined(_WIN32)
"%s\\oakengine_disk_test_default_XXXXXX",
#else
"%s/oakengine_disk_test_default_XXXXXX",
#endif
getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp");
#if defined(_WIN32)
char *tmpdir = _mktemp(tmp);
assert(tmpdir != NULL);
assert(_mkdir(tmpdir) == 0);
#else
char *tmpdir = mkdtemp(tmp);
assert(tmpdir != NULL);
#endif
assert(oakengine_disk_set_default_cache_path(tmpdir) == OAKENGINE_OK);
char updated[512];
assert(oakengine_disk_get_default_cache_path(updated, sizeof(updated)) > 0);
assert(strcmp(updated, tmpdir) == 0);
// Restore original default path.
assert(oakengine_disk_set_default_cache_path(original) == OAKENGINE_OK);
assert(oakengine_disk_get_default_cache_path(updated, sizeof(updated)) > 0);
assert(strcmp(updated, original) == 0);
#if defined(_WIN32)
_rmdir(tmpdir);
#else
rmdir(tmpdir);
#endif
}
int main(void)
{
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
test_instance_lifecycle();
test_default_cache_path();
test_open_folder_handle();
test_clear_cache();
test_settings_handler_round_trip();
test_set_default_cache_path();
test_invalidate_project();
// Leave DiskManager in the initialized state for shutdown.
oakengine_disk_create_instance();
oakengine_shutdown();
printf("oakengine_disk_test: OK\n");
return 0;
}
+659
View File
@@ -0,0 +1,659 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the liboakengine encoding facade
// (oakengine/encoding.h + oakengine/videoparams.h). Exercises the
// format/codec metadata queries, the image-sequence filename helpers, the
// scaling matrix, the OakEngineEncodingParams handle (getter/setter
// roundtrips, preset file load/save) and the VideoParams static data behind
// the standard combo boxes. No GPU: the export execution path itself is
// covered by oakengine_export_test; here only the error paths of
// render_with_params are touched (no sequence).
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
#include <direct.h>
#include <windows.h>
#else
#include <unistd.h>
#endif
#include "oakengine/encoding.h"
#include "oakengine/init.h"
#include "oakengine/videoparams.h"
static char g_tmpdir[4096];
static void make_tmpdir(void)
{
#if defined(_WIN32)
char base[MAX_PATH];
const DWORD len = GetTempPathA(MAX_PATH, base);
assert(len > 0 && len < MAX_PATH);
snprintf(g_tmpdir, sizeof(g_tmpdir), "%soakengine_encoding_test_%lu",
base, (unsigned long)GetCurrentProcessId());
assert(_mkdir(g_tmpdir) == 0);
#else
strcpy(g_tmpdir, "/tmp/oakengine_encoding_test_XXXXXX");
assert(mkdtemp(g_tmpdir) != NULL);
#endif
}
static void test_format_metadata(void)
{
char buf[256];
assert(oakengine_encoding_format_count() > 0);
// Matroska
assert(oakengine_encoding_format_name(OAKENGINE_ENCODING_FORMAT_MATROSKA,
buf, sizeof(buf)) > 0);
assert(strstr(buf, "Matroska") != NULL);
assert(oakengine_encoding_format_extension(
OAKENGINE_ENCODING_FORMAT_MATROSKA, buf, sizeof(buf)) > 0);
assert(strcmp(buf, "mkv") == 0);
// Invalid format
assert(oakengine_encoding_format_name(-1, buf, sizeof(buf)) == -1);
assert(oakengine_encoding_format_extension(9999, buf, sizeof(buf)) == -1);
assert(oakengine_encoding_format_video_codec_count(-1) == -1);
// MP4 carries H.264 video and AAC audio
const int vcount =
oakengine_encoding_format_video_codec_count(OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO);
assert(vcount > 0);
int found_h264 = 0;
for (int i = 0; i < vcount; i++) {
if (oakengine_encoding_format_video_codec_at(
OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO, i) ==
OAKENGINE_ENCODING_CODEC_H264) {
found_h264 = 1;
}
}
assert(found_h264);
assert(oakengine_encoding_format_video_codec_at(
OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO, vcount) == -1);
// WAV is audio-only and carries PCM
assert(oakengine_encoding_format_video_codec_count(
OAKENGINE_ENCODING_FORMAT_WAV) == 0);
const int acount = oakengine_encoding_format_audio_codec_count(
OAKENGINE_ENCODING_FORMAT_WAV);
assert(acount > 0);
int found_pcm = 0;
for (int i = 0; i < acount; i++) {
if (oakengine_encoding_format_audio_codec_at(
OAKENGINE_ENCODING_FORMAT_WAV, i) == OAKENGINE_ENCODING_CODEC_PCM) {
found_pcm = 1;
}
}
assert(found_pcm);
// SRT is subtitles-only
assert(oakengine_encoding_format_subtitle_codec_count(
OAKENGINE_ENCODING_FORMAT_SRT) > 0);
assert(oakengine_encoding_format_subtitle_codec_at(
OAKENGINE_ENCODING_FORMAT_SRT, 0) >= 0);
assert(oakengine_encoding_format_subtitle_codec_at(
OAKENGINE_ENCODING_FORMAT_SRT, -1) < 0);
assert(oakengine_encoding_format_audio_codec_count(
OAKENGINE_ENCODING_FORMAT_SRT) == 0);
}
static void test_codec_metadata(void)
{
char buf[256];
assert(oakengine_encoding_codec_name(OAKENGINE_ENCODING_CODEC_H264, buf,
sizeof(buf)) > 0);
assert(buf[0] != '\0');
assert(oakengine_encoding_codec_name(-1, buf, sizeof(buf)) == -1);
assert(oakengine_encoding_codec_is_still_image(5 /* PNG */) == 1);
assert(oakengine_encoding_codec_is_still_image(
OAKENGINE_ENCODING_CODEC_H264) == 0);
assert(oakengine_encoding_codec_is_lossless(OAKENGINE_ENCODING_CODEC_PCM) ==
1);
assert(oakengine_encoding_codec_is_lossless(OAKENGINE_ENCODING_CODEC_AAC) ==
0);
// Encoded pixel formats of H.264 in MP4: yuv420p is the preferred one
const int pcount = oakengine_encoding_pix_fmt_count(
OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO, OAKENGINE_ENCODING_CODEC_H264);
assert(pcount > 0);
assert(oakengine_encoding_pix_fmt_at(OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO,
OAKENGINE_ENCODING_CODEC_H264, 0, buf,
sizeof(buf)) > 0);
assert(strcmp(buf, "yuv420p") == 0);
assert(oakengine_encoding_pix_fmt_at(OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO,
OAKENGINE_ENCODING_CODEC_H264, pcount,
buf, sizeof(buf)) == -1);
assert(oakengine_encoding_pix_fmt_index(OAKENGINE_ENCODING_CODEC_H264,
"yuv420p") == 0);
assert(oakengine_encoding_pix_fmt_index(OAKENGINE_ENCODING_CODEC_H264,
"no-such-format") == 0);
assert(oakengine_encoding_pix_fmt_index(OAKENGINE_ENCODING_CODEC_H264,
NULL) == 0);
// Sample formats of PCM in WAV
const int scount = oakengine_encoding_sample_format_count(
OAKENGINE_ENCODING_FORMAT_WAV, OAKENGINE_ENCODING_CODEC_PCM);
assert(scount > 0);
for (int i = 0; i < scount; i++) {
assert(oakengine_encoding_sample_format_at(
OAKENGINE_ENCODING_FORMAT_WAV, OAKENGINE_ENCODING_CODEC_PCM,
i) >= 0);
}
assert(oakengine_encoding_sample_format_at(
OAKENGINE_ENCODING_FORMAT_WAV, OAKENGINE_ENCODING_CODEC_PCM,
scount) == -1);
}
static void test_filename_helpers(void)
{
char buf[4096];
assert(oakengine_encoding_filename_contains_digit_placeholder(
"/tmp/out_[#####].png") == 1);
assert(oakengine_encoding_filename_contains_digit_placeholder(
"/tmp/out.png") == 0);
assert(oakengine_encoding_filename_contains_digit_placeholder(NULL) == 0);
assert(oakengine_encoding_image_sequence_digit_count(
"/tmp/out_[#####].png") == 5);
assert(oakengine_encoding_image_sequence_digit_count("/tmp/out.png") == 0);
assert(oakengine_encoding_filename_remove_digit_placeholder(
"/tmp/out_[#####].png", buf, sizeof(buf)) > 0);
assert(strstr(buf, "[#####]") == NULL);
assert(strstr(buf, ".png") != NULL);
}
static void test_generate_matrix(void)
{
float m[16];
// Fit with matching dimensions is the identity
assert(oakengine_encoding_generate_matrix(OAKENGINE_ENCODING_SCALING_FIT,
1920, 1080, 1920, 1080,
m) == OAKENGINE_OK);
const float identity[16] = { 1, 0, 0, 0, 0, 1, 0, 0,
0, 0, 1, 0, 0, 0, 0, 1 };
for (int i = 0; i < 16; i++) {
assert(fabsf(m[i] - identity[i]) < 1e-6f);
}
// Stretch is the identity transform (the preview is normalized device
// coordinates; stretching needs no matrix)
assert(oakengine_encoding_generate_matrix(OAKENGINE_ENCODING_SCALING_STRETCH,
960, 540, 1920, 1080,
m) == OAKENGINE_OK);
for (int i = 0; i < 16; i++) {
assert(fabsf(m[i] - identity[i]) < 1e-6f);
}
// Fit into a wider-than-source frame pillarboxes: x scale shrinks to
// source_ar/export_ar
assert(oakengine_encoding_generate_matrix(OAKENGINE_ENCODING_SCALING_FIT,
1920, 1080, 1920, 540,
m) == OAKENGINE_OK);
const float expected_x = (1920.0f / 1080.0f) / (1920.0f / 540.0f);
assert(fabsf(m[0] - expected_x) < 1e-5f);
assert(fabsf(m[5] - 1.0f) < 1e-6f);
// Invalid arguments
assert(oakengine_encoding_generate_matrix(-1, 1, 1, 1, 1,
m) == OAKENGINE_E_INVALID);
assert(oakengine_encoding_generate_matrix(OAKENGINE_ENCODING_SCALING_FIT, 0,
1, 1, 1,
m) == OAKENGINE_E_INVALID);
}
static void test_params_handle(void)
{
char buf[1024];
OakEngineEncodingParams *p = oakengine_encoding_params_create();
assert(p != NULL);
// Fresh handle: nothing enabled, format unset
assert(oakengine_encoding_params_is_valid(p) == 0);
assert(oakengine_encoding_params_format(p) == -1);
assert(oakengine_encoding_params_video_enabled(p) == 0);
assert(oakengine_encoding_params_audio_enabled(p) == 0);
assert(oakengine_encoding_params_subtitles_enabled(p) == 0);
assert(oakengine_encoding_params_has_custom_range(p) == 0);
// Filename / format roundtrip
assert(oakengine_encoding_params_set_filename(p, "/tmp/out.mp4") ==
OAKENGINE_OK);
assert(oakengine_encoding_params_filename(p, buf, sizeof(buf)) > 0);
assert(strcmp(buf, "/tmp/out.mp4") == 0);
assert(oakengine_encoding_params_set_format(
p, OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO) == OAKENGINE_OK);
assert(oakengine_encoding_params_format(p) ==
OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO);
assert(oakengine_encoding_params_set_format(p, 9999) ==
OAKENGINE_E_INVALID);
// Video roundtrip
oak_video_params v = {};
v.width = 1920;
v.height = 1080;
v.time_base_num = 1001;
v.time_base_den = 30000;
v.format = 8; /* a PixelFormat::Format value */
v.pixel_aspect_num = 1;
v.pixel_aspect_den = 1;
v.interlacing = OAKENGINE_ENCODING_INTERLACE_BOTTOM_FIRST;
v.color_range = OAKENGINE_ENCODING_COLOR_RANGE_FULL;
v.divider = 1;
assert(oakengine_encoding_params_enable_video(
p, &v, OAKENGINE_ENCODING_CODEC_H264) == OAKENGINE_OK);
assert(oakengine_encoding_params_is_valid(p) == 1);
assert(oakengine_encoding_params_video_enabled(p) == 1);
assert(oakengine_encoding_params_video_codec(p) ==
OAKENGINE_ENCODING_CODEC_H264);
assert(oakengine_encoding_params_enable_video(p, NULL, 1) ==
OAKENGINE_E_INVALID);
assert(oakengine_encoding_params_enable_video(p, &v, 9999) ==
OAKENGINE_E_INVALID);
oak_video_params back = {};
assert(oakengine_encoding_params_get_video_params(p, &back) ==
OAKENGINE_OK);
assert(back.width == 1920 && back.height == 1080);
assert(back.time_base_num == 1001 && back.time_base_den == 30000);
assert(back.pixel_aspect_num == 1 && back.pixel_aspect_den == 1);
assert(back.interlacing == OAKENGINE_ENCODING_INTERLACE_BOTTOM_FIRST);
assert(back.color_range == OAKENGINE_ENCODING_COLOR_RANGE_FULL);
// Audio roundtrip
assert(oakengine_encoding_params_enable_audio(p, 48000, 0x3, 4,
OAKENGINE_ENCODING_CODEC_AAC) ==
OAKENGINE_OK);
assert(oakengine_encoding_params_audio_enabled(p) == 1);
assert(oakengine_encoding_params_audio_codec(p) ==
OAKENGINE_ENCODING_CODEC_AAC);
int sample_rate = 0, sample_format = 0;
uint64_t layout = 0;
assert(oakengine_encoding_params_get_audio_params(p, &sample_rate, &layout,
&sample_format) ==
OAKENGINE_OK);
assert(sample_rate == 48000 && layout == 0x3 && sample_format == 4);
assert(oakengine_encoding_params_enable_audio(p, 0, 0x3, 4, 0) ==
OAKENGINE_E_INVALID);
// Subtitles (embedded, then sidecar)
assert(oakengine_encoding_params_enable_subtitles(
p, OAKENGINE_ENCODING_CODEC_SRT) == OAKENGINE_OK);
assert(oakengine_encoding_params_subtitles_enabled(p) == 1);
assert(oakengine_encoding_params_subtitles_are_sidecar(p) == 0);
assert(oakengine_encoding_params_subtitles_codec(p) ==
OAKENGINE_ENCODING_CODEC_SRT);
assert(oakengine_encoding_params_enable_sidecar_subtitles(
p, OAKENGINE_ENCODING_FORMAT_SRT,
OAKENGINE_ENCODING_CODEC_SRT) == OAKENGINE_OK);
assert(oakengine_encoding_params_subtitles_are_sidecar(p) == 1);
assert(oakengine_encoding_params_subtitles_sidecar_format(p) ==
OAKENGINE_ENCODING_FORMAT_SRT);
// Scalar setters/getters
oakengine_encoding_params_set_video_bit_rate(p, 8000000);
assert(oakengine_encoding_params_video_bit_rate(p) == 8000000);
oakengine_encoding_params_set_video_min_bit_rate(p, 1000);
assert(oakengine_encoding_params_video_min_bit_rate(p) == 1000);
oakengine_encoding_params_set_video_max_bit_rate(p, 16000000);
assert(oakengine_encoding_params_video_max_bit_rate(p) == 16000000);
oakengine_encoding_params_set_video_buffer_size(p, 2000000);
assert(oakengine_encoding_params_video_buffer_size(p) == 2000000);
oakengine_encoding_params_set_video_threads(p, 4);
assert(oakengine_encoding_params_video_threads(p) == 4);
oakengine_encoding_params_set_audio_bit_rate(p, 320000);
assert(oakengine_encoding_params_audio_bit_rate(p) == 320000);
assert(oakengine_encoding_params_set_video_pix_fmt(p, "yuv420p") ==
OAKENGINE_OK);
assert(oakengine_encoding_params_video_pix_fmt(p, buf, sizeof(buf)) > 0);
assert(strcmp(buf, "yuv420p") == 0);
oakengine_encoding_params_set_video_is_image_sequence(p, 1);
assert(oakengine_encoding_params_video_is_image_sequence(p) == 1);
oakengine_encoding_params_set_video_is_image_sequence(p, 0);
assert(oakengine_encoding_params_video_is_image_sequence(p) == 0);
assert(oakengine_encoding_params_set_color_transform(p, "sRGB OETF") ==
OAKENGINE_OK);
assert(oakengine_encoding_params_color_transform_output(p, buf,
sizeof(buf)) > 0);
assert(strcmp(buf, "sRGB OETF") == 0);
oakengine_encoding_params_set_export_length(p, 10, 1);
int num = 0, den = 0;
assert(oakengine_encoding_params_get_export_length(p, &num, &den) ==
OAKENGINE_OK);
assert(num == 10 && den == 1);
// Custom range
oakengine_encoding_params_set_custom_range(p, 1, 1, 5, 1);
assert(oakengine_encoding_params_has_custom_range(p) == 1);
int64_t in_num = 0, in_den = 0, out_num = 0, out_den = 0;
assert(oakengine_encoding_params_get_custom_range(p, &in_num, &in_den,
&out_num,
&out_den) == OAKENGINE_OK);
assert(in_num == 1 && in_den == 1 && out_num == 5 && out_den == 1);
// Scaling method
assert(oakengine_encoding_params_set_video_scaling_method(
p, OAKENGINE_ENCODING_SCALING_CROP) == OAKENGINE_OK);
assert(oakengine_encoding_params_video_scaling_method(p) ==
OAKENGINE_ENCODING_SCALING_CROP);
assert(oakengine_encoding_params_set_video_scaling_method(p, 42) ==
OAKENGINE_E_INVALID);
// Video options
assert(oakengine_encoding_params_video_option(p, "crf", buf,
sizeof(buf)) ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_encoding_params_set_video_option(p, "crf", "18") ==
OAKENGINE_OK);
assert(oakengine_encoding_params_video_option(p, "crf", buf,
sizeof(buf)) > 0);
assert(strcmp(buf, "18") == 0);
// Disables
oakengine_encoding_params_disable_subtitles(p);
assert(oakengine_encoding_params_subtitles_enabled(p) == 0);
oakengine_encoding_params_disable_video(p);
assert(oakengine_encoding_params_video_enabled(p) == 0);
assert(oakengine_encoding_params_get_video_params(p, &back) ==
OAKENGINE_E_STATE);
oakengine_encoding_params_disable_audio(p);
assert(oakengine_encoding_params_audio_enabled(p) == 0);
// NULL safety
oakengine_encoding_params_destroy(NULL);
assert(oakengine_encoding_params_is_valid(NULL) == 0);
oakengine_encoding_params_destroy(p);
}
static void test_preset_files(void)
{
char buf[1024];
// Preset directory listing is readable (may be empty in the sandbox)
assert(oakengine_encoding_preset_path(buf, sizeof(buf)) > 0);
const int count = oakengine_encoding_preset_count();
assert(count >= 0);
for (int i = 0; i < count; i++) {
assert(oakengine_encoding_preset_name(i, buf, sizeof(buf)) > 0);
}
assert(oakengine_encoding_preset_name(count, buf, sizeof(buf)) == -1);
// Save/load roundtrip through a temp file
char path[4096];
snprintf(path, sizeof(path), "%s/preset.xml", g_tmpdir);
OakEngineEncodingParams *p = oakengine_encoding_params_create();
assert(oakengine_encoding_params_set_filename(p, "/tmp/out.mp4") ==
OAKENGINE_OK);
assert(oakengine_encoding_params_set_format(
p, OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO) == OAKENGINE_OK);
oak_video_params v = {};
v.width = 1280;
v.height = 720;
v.time_base_num = 1;
v.time_base_den = 25;
v.format = 8;
v.pixel_aspect_num = 1;
v.pixel_aspect_den = 1;
v.interlacing = OAKENGINE_ENCODING_INTERLACE_NONE;
v.color_range = OAKENGINE_ENCODING_COLOR_RANGE_LIMITED;
v.divider = 1;
assert(oakengine_encoding_params_enable_video(
p, &v, OAKENGINE_ENCODING_CODEC_H264) == OAKENGINE_OK);
assert(oakengine_encoding_params_set_video_option(p, "crf", "20") ==
OAKENGINE_OK);
assert(oakengine_encoding_params_save_file(p, path) == OAKENGINE_OK);
oakengine_encoding_params_destroy(p);
OakEngineEncodingParams *q = oakengine_encoding_params_create();
assert(oakengine_encoding_params_load_file(q, path) == OAKENGINE_OK);
assert(oakengine_encoding_params_video_enabled(q) == 1);
assert(oakengine_encoding_params_video_codec(q) ==
OAKENGINE_ENCODING_CODEC_H264);
oak_video_params back = {};
assert(oakengine_encoding_params_get_video_params(q, &back) ==
OAKENGINE_OK);
assert(back.width == 1280 && back.height == 720);
assert(back.time_base_num == 1 && back.time_base_den == 25);
assert(oakengine_encoding_params_video_option(q, "crf", buf,
sizeof(buf)) > 0);
assert(strcmp(buf, "20") == 0);
oakengine_encoding_params_destroy(q);
// Loading a nonexistent file fails
OakEngineEncodingParams *r = oakengine_encoding_params_create();
assert(oakengine_encoding_params_load_file(r, "/no/such/file.xml") ==
OAKENGINE_E_FAILED);
oakengine_encoding_params_destroy(r);
// Bad arguments
assert(oakengine_encoding_params_save_file(NULL, path) ==
OAKENGINE_E_INVALID);
}
static void test_last_used_and_render_errors(void)
{
// NULL sequence: no last-used params, no-op setter
assert(oakengine_encoding_params_get_last_used(NULL) == NULL);
oakengine_encoding_params_set_last_used(NULL, NULL);
// render_with_params without a valid sequence/params fails cleanly
assert(oakengine_export_render_with_params(NULL, NULL) ==
OAKENGINE_E_INVALID);
OakEngineEncodingParams *p = oakengine_encoding_params_create();
assert(oakengine_export_render_with_params(NULL, p) ==
OAKENGINE_E_INVALID);
// Nothing enabled on the handle
assert(oakengine_export_render_with_params(
reinterpret_cast<OakEngineSequence *>(p), p) ==
OAKENGINE_E_INVALID);
oakengine_encoding_params_destroy(p);
// Audio recording requires an enabled audio track on the handle
assert(oakengine_encoding_start_audio_recording(NULL, NULL, 0) ==
OAKENGINE_E_INVALID);
}
static void test_video_params_statics(void)
{
char buf[256];
int num = 0, den = 0;
// Standard frame rates
const int fr_count = oakengine_video_params_supported_frame_rate_count();
assert(fr_count > 0);
for (int i = 0; i < fr_count; i++) {
assert(oakengine_video_params_supported_frame_rate_at(i, &num, &den) ==
OAKENGINE_OK);
assert(num > 0 && den > 0);
}
assert(oakengine_video_params_supported_frame_rate_at(fr_count, &num,
&den) ==
OAKENGINE_E_INVALID);
// 24000/1001 prints as 23.976...
assert(oakengine_video_params_frame_rate_to_string(24000, 1001, buf,
sizeof(buf)) > 0);
assert(strstr(buf, "23.97") != NULL);
// Standard pixel aspects: the first one is square (1:1)
const int pa_count = oakengine_video_params_standard_pixel_aspect_count();
assert(pa_count > 0);
assert(oakengine_video_params_standard_pixel_aspect_at(0, &num, &den) ==
OAKENGINE_OK);
assert(num == 1 && den == 1);
assert(oakengine_video_params_standard_pixel_aspect_name(0, buf,
sizeof(buf)) > 0);
assert(buf[0] != '\0');
assert(oakengine_video_params_standard_pixel_aspect_at(pa_count, &num,
&den) ==
OAKENGINE_E_INVALID);
// Custom PAR label template
assert(oakengine_video_params_format_pixel_aspect_ratio_string(
"Custom (%1)", 32, 27, buf, sizeof(buf)) > 0);
assert(strstr(buf, "Custom") != NULL);
// Dividers
const int div_count = oakengine_video_params_supported_divider_count();
assert(div_count > 0);
for (int i = 0; i < div_count; i++) {
const int d = oakengine_video_params_supported_divider_at(i);
assert(d > 0);
assert(oakengine_video_params_divider_name(d, buf, sizeof(buf)) > 0);
}
assert(oakengine_video_params_supported_divider_at(div_count) == -1);
// Pixel format names: some entry must be non-empty
assert(oakengine_video_params_pixel_format_name(8, buf, sizeof(buf)) > 0);
// Float detection (8-bit integer formats are not float)
assert(oakengine_video_params_format_is_float(0) == 0);
// Effective (divider-scaled) size
int w = 0, h = 0;
assert(oakengine_video_params_effective_size(1920, 1080, 2, &w, &h) ==
OAKENGINE_OK);
assert(w == 960 && h == 540);
assert(oakengine_video_params_effective_size(0, 1080, 2, &w, &h) ==
OAKENGINE_E_INVALID);
}
// POD mirror of the display-path VideoParams (B7): make/equal/is_valid,
// bytes-per-pixel and the internal channel count.
static void test_video_params_pod(void)
{
oak_video_params p, q;
// make fills every field
assert(oakengine_video_params_make(&p, 1920, 1080, 1001, 30000, 0, 1, 1,
0, 0, 2) == OAKENGINE_OK);
assert(p.width == 1920 && p.height == 1080);
assert(p.time_base_num == 1001 && p.time_base_den == 30000);
assert(p.pixel_aspect_num == 1 && p.pixel_aspect_den == 1);
assert(p.divider == 2);
assert(oakengine_video_params_make(NULL, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1) ==
OAKENGINE_E_INVALID);
// equal: identical PODs match, any single-field difference does not
q = p;
assert(oakengine_video_params_equal(&p, &q) == 1);
assert(oakengine_video_params_equal(&p, NULL) == 0);
assert(oakengine_video_params_equal(NULL, &q) == 0);
q.divider = 1;
assert(oakengine_video_params_equal(&p, &q) == 0);
q = p;
q.interlacing = 1;
assert(oakengine_video_params_equal(&p, &q) == 0);
// is_valid: positive dimensions + in-range format passes; zero
// dimensions or an out-of-range format fail
assert(oakengine_video_params_is_valid(&p) == 1);
assert(oakengine_video_params_is_valid(NULL) == 0);
q = p;
q.width = 0;
assert(oakengine_video_params_is_valid(&q) == 0);
q = p;
q.format = -1; // olive::PixelFormat::invalid
assert(oakengine_video_params_is_valid(&q) == 0);
// bytes per pixel: u8 RGBA = 4, f32 RGBA = 16 (format values follow
// olive::PixelFormat::Format: 0 = u8, 4 = f32)
const int channels = oakengine_video_params_internal_channel_count();
assert(channels == 4);
assert(oakengine_video_params_bytes_per_pixel(0, channels) == 4);
assert(oakengine_video_params_bytes_per_pixel(4, channels) == 16);
}
// Engine-side VideoParams construction used by the app during R6 to avoid
// pulling C++ constructors into oak-editor.
static void test_video_params_create_free(void)
{
// NULL pod -> NULL handle
assert(oakengine_video_params_create(NULL) == NULL);
// Empty POD -> default-constructed VideoParams handle
oak_video_params empty = {};
void *vp_empty = oakengine_video_params_create(&empty);
assert(vp_empty != NULL);
oakengine_video_params_free(vp_empty);
// Display-path POD with explicit timebase
oak_video_params pod;
assert(oakengine_video_params_make(&pod, 1920, 1080, 1001, 30000, 0, 1, 1,
0, 0, 1) == OAKENGINE_OK);
void *vp = oakengine_video_params_create(&pod);
assert(vp != NULL);
oakengine_video_params_free(vp);
// Display-path POD without timebase (uses constructor without timebase)
oak_video_params pod2 = {};
pod2.width = 640;
pod2.height = 480;
pod2.format = 0; // u8
void *vp2 = oakengine_video_params_create(&pod2);
assert(vp2 != NULL);
oakengine_video_params_free(vp2);
// free(NULL) is a no-op
oakengine_video_params_free(NULL);
}
int main(void)
{
make_tmpdir();
// HEADLESS: no GL, but a QCoreApplication (needed by the FFmpeg encoder
// probes and QStandardPaths behind the metadata queries) comes up.
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
test_format_metadata();
test_codec_metadata();
test_filename_helpers();
test_generate_matrix();
test_params_handle();
test_preset_files();
test_last_used_and_render_errors();
test_video_params_statics();
test_video_params_create_free();
oakengine_shutdown();
printf("oakengine_encoding_test: OK\n");
return 0;
}
+960
View File
@@ -0,0 +1,960 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the liboakengine event subscription family
// (oakengine/events.h) and the track block traversal family
// (oakengine_track_nearest_block_* / oakengine_block_*). Every subscription
// is exercised by provoking a real engine change through the facade and
// asserting the callback fired with the documented payload. No GL required.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
#include <direct.h>
#include <windows.h>
#else
#include <unistd.h>
#endif
#include "oakengine/events.h"
#include "oakengine/footage.h"
#include "oakengine/init.h"
#include "oakengine/node.h"
#include "oakengine/project.h"
#include "oakengine/timeline.h"
#include "oakengine/viewer.h"
#ifndef OAK_TEST_SOURCE_DIR
#define OAK_TEST_SOURCE_DIR "."
#endif
static char g_tmpdir[4096];
static void make_tmpdir(void)
{
#if defined(_WIN32)
char base[MAX_PATH];
const DWORD len = GetTempPathA(MAX_PATH, base);
assert(len > 0 && len < MAX_PATH);
snprintf(g_tmpdir, sizeof(g_tmpdir), "%soakengine_events_test_%lu", base,
(unsigned long)GetCurrentProcessId());
assert(_mkdir(g_tmpdir) == 0);
#else
strcpy(g_tmpdir, "/tmp/oakengine_events_test_XXXXXX");
assert(mkdtemp(g_tmpdir) != NULL);
#endif
}
static void demo_path(char *dst, size_t cap)
{
const int n = snprintf(dst, cap, "%s/tests/demo.mp4", OAK_TEST_SOURCE_DIR);
assert(n > 0 && (size_t)n < cap);
}
// Callback recorder: counts deliveries per event id and keeps the last
// payload of each.
#define MAX_TRACKED_EVENT 128
typedef struct {
int count[MAX_TRACKED_EVENT];
int64_t last_a[MAX_TRACKED_EVENT];
int64_t last_b[MAX_TRACKED_EVENT];
int64_t last_c[MAX_TRACKED_EVENT];
void *last_source[MAX_TRACKED_EVENT];
void *last_handle[MAX_TRACKED_EVENT];
char last_s[MAX_TRACKED_EVENT][256];
} EventLog;
static void record_event(const oakengine_event *event, void *userdata)
{
EventLog *log = (EventLog *)userdata;
assert(event != NULL);
assert(event->id > 0 && event->id < MAX_TRACKED_EVENT);
log->count[event->id]++;
log->last_a[event->id] = event->a;
log->last_b[event->id] = event->b;
log->last_c[event->id] = event->c;
log->last_source[event->id] = event->source;
log->last_handle[event->id] = event->handle;
snprintf(log->last_s[event->id], sizeof(log->last_s[event->id]), "%s",
event->s ? event->s : "");
}
static void reset_event(EventLog *log, int id)
{
log->count[id] = 0;
}
// ---- Subscription validation ----------------------------------------------
static void test_subscribe_validation(OakEngineProject *project,
OakEngineSequence *seq,
OakEngineTrack *track)
{
EventLog log;
memset(&log, 0, sizeof(log));
// NULL handle / NULL callback / unknown event id.
assert(oakengine_event_subscribe(
NULL, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED, record_event,
&log) == 0);
assert(oakengine_event_subscribe(project,
OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED,
NULL, &log) == 0);
assert(oakengine_event_subscribe(project, 999, record_event, &log) == 0);
// Handle/event family mismatches.
assert(oakengine_event_subscribe(
seq, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED, record_event,
&log) == 0);
assert(oakengine_event_subscribe(project,
OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED,
record_event, &log) == 0);
assert(oakengine_event_subscribe(track,
OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM,
record_event, &log) == 0);
assert(oakengine_event_subscribe(project,
OAKENGINE_EVENT_TRACK_BLOCK_ADDED,
record_event, &log) == 0);
// Bad unsubscribe arguments.
assert(oakengine_event_unsubscribe(0) == OAKENGINE_E_INVALID);
assert(oakengine_event_unsubscribe(-5) == OAKENGINE_E_INVALID);
assert(oakengine_event_unsubscribe(424242) == OAKENGINE_E_NOT_FOUND);
}
// ---- Project events ---------------------------------------------------------
static void test_project_events(OakEngineProject *project)
{
EventLog log;
memset(&log, 0, sizeof(log));
// Normalize to unmodified first: modified_changed only fires on an
// actual flip, and the setup above already dirtied the project.
oakengine_project_set_modified(project, 0);
int64_t sub = oakengine_event_subscribe(
project, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED, record_event, &log);
assert(sub > 0);
oakengine_project_set_modified(project, 1);
assert(log.count[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] == 1);
assert(log.last_a[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] == 1);
assert(log.last_source[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] ==
(void *)project);
oakengine_project_set_modified(project, 0);
assert(log.count[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] == 2);
assert(log.last_a[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] == 0);
// After unsubscribing no further events arrive.
assert(oakengine_event_unsubscribe(sub) == OAKENGINE_OK);
oakengine_project_set_modified(project, 1);
assert(log.count[OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED] == 2);
// Unsubscribing twice is a not-found no-op.
assert(oakengine_event_unsubscribe(sub) == OAKENGINE_E_NOT_FOUND);
}
// ---- Folder events ----------------------------------------------------------
static void test_folder_events(OakEngineProject *project)
{
EventLog log;
memset(&log, 0, sizeof(log));
// A fresh project's first node is its root folder (same fixture as
// oakengine_footage_test).
OakEngineNode *root = oakengine_project_node_at(project, 0);
assert(root != NULL);
int64_t sub_begin = oakengine_event_subscribe(
root, OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM, record_event, &log);
int64_t sub_end = oakengine_event_subscribe(
root, OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM, record_event, &log);
int64_t sub_rm_begin = oakengine_event_subscribe(
root, OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM, record_event, &log);
int64_t sub_rm_end = oakengine_event_subscribe(
root, OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM, record_event, &log);
assert(sub_begin > 0 && sub_end > 0 && sub_rm_begin > 0 &&
sub_rm_end > 0);
OakEngineNode *folder = oakengine_folder_create(project, root, "Sub");
assert(folder != NULL);
assert(log.count[OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM] == 1);
assert(log.last_handle[OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM] ==
(void *)folder);
assert(log.last_a[OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM] >= 0);
assert(log.count[OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM] == 1);
// Undoing the folder creation removes it from the root again.
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM] == 1);
assert(log.last_handle[OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM] ==
(void *)folder);
assert(log.count[OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM] == 1);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_begin) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_end) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_rm_begin) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_rm_end) == OAKENGINE_OK);
}
// ---- Sequence / track events -------------------------------------------------
static void test_sequence_events(OakEngineProject *project,
OakEngineSequence *seq,
const char *media_path)
{
EventLog log;
memset(&log, 0, sizeof(log));
// Track added: subscribe, then append an audio track.
int64_t sub_track = oakengine_event_subscribe(
seq, OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED, record_event, &log);
assert(sub_track > 0);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
0);
assert(log.count[OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED] == 1);
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED] ==
OAKENGINE_TRACK_TYPE_AUDIO);
assert(log.last_handle[OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED] != NULL);
assert(log.last_source[OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED] ==
(void *)seq);
assert(oakengine_event_unsubscribe(sub_track) == OAKENGINE_OK);
// Block added on the video track (track 0 was created by the caller).
OakEngineTrack *track = oakengine_sequence_track_at(
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0);
assert(track != NULL);
int64_t sub_block = oakengine_event_subscribe(
track, OAKENGINE_EVENT_TRACK_BLOCK_ADDED, record_event, &log);
assert(sub_block > 0);
OakEngineFootage *footage =
oakengine_project_import_footage(project, media_path);
assert(footage != NULL);
OakEngineClip *clip = oakengine_sequence_add_footage_clip(
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 10, 40, 5);
assert(clip != NULL);
// Placing at in=10 on an empty track inserts a leading gap first, so the
// event fires twice (gap 0..10, then the clip 10..40); the last
// delivery is the clip itself.
assert(log.count[OAKENGINE_EVENT_TRACK_BLOCK_ADDED] == 2);
assert(log.last_handle[OAKENGINE_EVENT_TRACK_BLOCK_ADDED] ==
(void *)clip);
assert(log.last_a[OAKENGINE_EVENT_TRACK_BLOCK_ADDED] == 10);
assert(log.last_b[OAKENGINE_EVENT_TRACK_BLOCK_ADDED] == 40);
assert(log.last_source[OAKENGINE_EVENT_TRACK_BLOCK_ADDED] ==
(void *)track);
assert(oakengine_event_unsubscribe(sub_block) == OAKENGINE_OK);
oakengine_footage_free(footage);
// Marker added / modified.
int64_t sub_marker_add = oakengine_event_subscribe(
seq, OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED, record_event, &log);
int64_t sub_marker_mod = oakengine_event_subscribe(
seq, OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED, record_event, &log);
assert(sub_marker_add > 0 && sub_marker_mod > 0);
assert(oakengine_sequence_marker_add(seq, 7, "Mark") == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED] == 1);
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED] == 7);
assert(oakengine_sequence_marker_rename(seq, 7, "Renamed") ==
OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED] == 1);
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED] == 7);
assert(oakengine_event_unsubscribe(sub_marker_add) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_marker_mod) == OAKENGINE_OK);
// Workarea enabled + range changed.
int64_t sub_range = oakengine_event_subscribe(
seq, OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED, record_event,
&log);
int64_t sub_enabled = oakengine_event_subscribe(
seq, OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED, record_event,
&log);
assert(sub_range > 0 && sub_enabled > 0);
assert(oakengine_sequence_set_workarea(seq, 1, 3, 21) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED] ==
1);
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED] ==
1);
assert(log.count[OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED] == 1);
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED] == 3);
assert(log.last_b[OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED] ==
21);
assert(oakengine_event_unsubscribe(sub_range) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_enabled) == OAKENGINE_OK);
}
// ---- Track block traversal ---------------------------------------------------
static void test_block_traversal(OakEngineSequence *seq)
{
OakEngineTrack *track = oakengine_sequence_track_at(
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0);
assert(track != NULL);
// The caller placed one clip at 10..40; the track chain is
// gap(0..10) -> clip(10..40).
assert(oakengine_track_block_count(track) == 2);
OakEngineBlock *gap =
oakengine_track_nearest_block_before_or_at(track, 0);
assert(gap != NULL);
assert(oakengine_block_is_gap(gap) == 1);
OakEngineBlock *clip = oakengine_block_next(gap);
assert(clip != NULL);
assert(oakengine_block_is_gap(clip) == 0);
assert(oakengine_block_next(clip) == NULL);
assert(oakengine_block_prev(clip) == gap);
assert(oakengine_block_prev(gap) == NULL);
int64_t in = -1, out = -1;
assert(oakengine_block_get_range(gap, &in, &out) == OAKENGINE_OK);
assert(in == 0 && out == 10);
assert(oakengine_block_get_range(clip, &in, &out) == OAKENGINE_OK);
assert(in == 10 && out == 40);
// Time queries.
assert(oakengine_track_block_at_time(track, 15) == clip);
assert(oakengine_track_block_at_time(track, 5) == gap);
assert(oakengine_track_block_at_time(track, 40) == NULL);
assert(oakengine_track_nearest_block_before(track, 10) == gap);
assert(oakengine_track_nearest_block_before_or_at(track, 10) == clip);
assert(oakengine_track_nearest_block_after(track, 0) == clip);
assert(oakengine_track_nearest_block_after_or_at(track, 10) == clip);
assert(oakengine_track_nearest_block_after(track, 10) == NULL);
// NULL safety.
assert(oakengine_track_block_count(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_track_block_at_time(NULL, 0) == NULL);
assert(oakengine_track_nearest_block_before(NULL, 0) == NULL);
assert(oakengine_track_nearest_block_before_or_at(NULL, 0) == NULL);
assert(oakengine_track_nearest_block_after(NULL, 0) == NULL);
assert(oakengine_track_nearest_block_after_or_at(NULL, 0) == NULL);
assert(oakengine_block_next(NULL) == NULL);
assert(oakengine_block_prev(NULL) == NULL);
assert(oakengine_block_is_gap(NULL) == 0);
assert(oakengine_block_get_range(NULL, &in, &out) ==
OAKENGINE_E_INVALID);
}
// ---- Node events (B8a) ------------------------------------------------------
static void test_node_events(OakEngineProject *project)
{
EventLog log;
memset(&log, 0, sizeof(log));
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
OakEngineNode *lut = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.ociolut");
OakEngineNode *text = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.text3");
assert(solid != NULL && lut != NULL && text != NULL);
// Family mismatch: node events need a node handle.
assert(oakengine_event_subscribe(
project, OAKENGINE_EVENT_NODE_LABEL_CHANGED, record_event,
&log) == 0);
int64_t subs[16];
int nsubs = 0;
subs[nsubs++] = oakengine_event_subscribe(
solid, OAKENGINE_EVENT_NODE_LABEL_CHANGED, record_event, &log);
subs[nsubs++] = oakengine_event_subscribe(
solid, OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED, record_event, &log);
subs[nsubs++] = oakengine_event_subscribe(
lut, OAKENGINE_EVENT_NODE_INPUT_CONNECTED, record_event, &log);
subs[nsubs++] = oakengine_event_subscribe(
lut, OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED, record_event, &log);
subs[nsubs++] = oakengine_event_subscribe(
solid, OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED, record_event,
&log);
subs[nsubs++] = oakengine_event_subscribe(
text, OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED, record_event,
&log);
subs[nsubs++] = oakengine_event_subscribe(
solid, OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED, record_event,
&log);
subs[nsubs++] = oakengine_event_subscribe(
solid, OAKENGINE_EVENT_NODE_KEYFRAME_ADDED, record_event, &log);
subs[nsubs++] = oakengine_event_subscribe(
solid, OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED, record_event, &log);
subs[nsubs++] = oakengine_event_subscribe(
solid, OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED, record_event,
&log);
subs[nsubs++] = oakengine_event_subscribe(
solid, OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED, record_event,
&log);
subs[nsubs++] = oakengine_event_subscribe(
solid, OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED, record_event,
&log);
for (int i = 0; i < nsubs; i++) {
assert(subs[i] > 0);
}
// Label.
assert(oakengine_node_set_label(solid, "EventSolid") == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_LABEL_CHANGED] == 1);
assert(strcmp(log.last_s[OAKENGINE_EVENT_NODE_LABEL_CHANGED],
"EventSolid") == 0);
assert(log.last_source[OAKENGINE_EVENT_NODE_LABEL_CHANGED] == solid);
// Value change on an input: element -1, a valid range, the input id.
oak_node_value v;
memset(&v, 0, sizeof(v));
v.type = OAK_NODE_VALUE_COLOR;
v.f[0] = 0.5;
v.f[3] = 1.0;
assert(oakengine_node_set_input(solid, "color_in", &v) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED] >= 1);
assert(strcmp(log.last_s[OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED],
"color_in") == 0);
assert(log.last_a[OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED] == -1);
// Edge connect/disconnect: output node in the handle field.
assert(oakengine_node_connect(solid, lut, "tex_in") == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_CONNECTED] == 1);
assert(log.last_handle[OAKENGINE_EVENT_NODE_INPUT_CONNECTED] == solid);
assert(strcmp(log.last_s[OAKENGINE_EVENT_NODE_INPUT_CONNECTED],
"tex_in") == 0);
assert(oakengine_node_disconnect(lut, "tex_in") == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED] == 1);
// Property change (notified write only).
assert(oakengine_node_set_input_property_string(solid, "color_in",
"my_prop", "1", 1) ==
OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED] == 1);
assert(strcmp(log.last_s[OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED],
"color_in") == 0);
assert(oakengine_node_set_input_property_string(solid, "color_in",
"my_prop", "2", 0) ==
OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED] == 1);
// Array size change: old and new sizes.
const int arr_before = oakengine_node_input_array_size(text, "args_in");
assert(oakengine_node_array_insert_at(text, "args_in", 0) ==
OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED] == 1);
assert(log.last_a[OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED] ==
arr_before);
assert(log.last_b[OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED] ==
arr_before + 1);
// Keyframe enable + add/remove/time/type/value.
reset_event(&log, OAKENGINE_EVENT_NODE_KEYFRAME_ADDED);
assert(oakengine_node_set_input_keyframing(solid, "color_in", -1, 1, 0,
1, NULL) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED] == 1);
assert(log.last_b[OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED] == 1);
assert(strcmp(log.last_s[OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED],
"color_in") == 0);
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_ADDED] >= 1);
assert(log.last_handle[OAKENGINE_EVENT_NODE_KEYFRAME_ADDED] != NULL);
// COLOR has four tracks; enabling keyframing adds one key per track.
assert(log.last_b[OAKENGINE_EVENT_NODE_KEYFRAME_ADDED] == 3);
// Type change on the created key (ts 0 = time 0s).
reset_event(&log, OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED);
int64_t times[1] = { 0 };
int tracks[1] = { 0 };
// The engine only emits the type-changed signal on multi-key tracks,
// so add a second key (1s = ts 30 with the default timebase) first.
assert(oakengine_node_keyframes_toggle_at_time(solid, "color_in", -1, 1,
1, 1, NULL) ==
OAKENGINE_OK);
// Default type is bezier; switch to hold (type 2) for a real change.
assert(oakengine_node_keyframes_set_type_many(solid, "color_in", -1,
times, tracks, 1, 2) == 1);
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED] == 1);
// Value change.
reset_event(&log, OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED);
oak_node_value nv;
memset(&nv, 0, sizeof(nv));
nv.type = OAK_NODE_VALUE_COLOR;
nv.f[0] = 0.75;
assert(oakengine_node_keyframes_set_value_many(solid, "color_in", -1,
times, tracks, 1, &nv,
NULL) == 1);
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED] == 1);
// Time change.
reset_event(&log, OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED);
assert(oakengine_node_keyframes_set_time_many(solid, "color_in", -1,
times, tracks, 1,
30) == 1);
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED] == 1);
// Removal.
reset_event(&log, OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED);
assert(oakengine_node_set_input_keyframing(solid, "color_in", -1, 0, 0,
1, NULL) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED] >= 1);
assert(log.count[OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED] == 2);
for (int i = 0; i < nsubs; i++) {
assert(oakengine_event_unsubscribe(subs[i]) == OAKENGINE_OK);
}
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, text) == OAKENGINE_OK);
}
// ---- Group + context position events -----------------------------------------
static void test_group_events(OakEngineProject *project)
{
EventLog log;
memset(&log, 0, sizeof(log));
OakEngineNode *group = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.group");
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(group != NULL && solid != NULL);
int64_t subs[4];
int nsubs = 0;
subs[nsubs++] = oakengine_event_subscribe(
group, OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED, record_event,
&log);
subs[nsubs++] = oakengine_event_subscribe(
group, OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED, record_event,
&log);
subs[nsubs++] = oakengine_event_subscribe(
group, OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED, record_event,
&log);
subs[nsubs++] = oakengine_event_subscribe(
group, OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED, record_event,
&log);
for (int i = 0; i < nsubs; i++) {
assert(subs[i] > 0);
}
// The group must contain the inner node before a passthrough can be
// added (insertion itself fires the position-changed event).
assert(oakengine_node_set_context_position(group, solid, 0.0, 0.0) ==
OAKENGINE_OK);
reset_event(&log, OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED);
// Add passthrough: handle = inner node, s = input id, a = element.
assert(oakengine_group_add_input_passthrough(group, solid, "color_in",
-1, NULL, NULL, 0) > 0);
assert(log.count[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED] == 1);
assert(log.last_source[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED] ==
group);
assert(log.last_handle[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED] ==
solid);
assert(strcmp(log.last_s[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED],
"color_in") == 0);
assert(log.last_a[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED] == -1);
// Output passthrough: handle = the new output node.
assert(oakengine_group_set_output_passthrough(group, solid) ==
OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED] == 1);
assert(log.last_handle[OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED] ==
solid);
// Remove passthrough.
assert(oakengine_group_remove_input_passthrough(group, solid, "color_in",
-1) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED] == 1);
assert(log.last_handle[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED] ==
solid);
assert(strcmp(log.last_s[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED],
"color_in") == 0);
assert(log.last_a[OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED] == -1);
// Context position: handle = child node, a/b = x/y double bit patterns.
assert(oakengine_node_set_context_position(group, solid, 3.5, -2.25) ==
OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED] == 1);
assert(log.last_source[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED] ==
group);
assert(log.last_handle[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED] ==
solid);
double px, py;
memcpy(&px, &log.last_a[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED],
sizeof(px));
memcpy(&py, &log.last_b[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED],
sizeof(py));
assert(px == 3.5 && py == -2.25);
// Moving again re-emits with the new coordinates.
assert(oakengine_node_set_context_position(group, solid, 0.0, 1.0) ==
OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED] == 2);
memcpy(&px, &log.last_a[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED],
sizeof(px));
memcpy(&py, &log.last_b[OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED],
sizeof(py));
assert(px == 0.0 && py == 1.0);
for (int i = 0; i < nsubs; i++) {
assert(oakengine_event_unsubscribe(subs[i]) == OAKENGINE_OK);
}
assert(oakengine_project_remove_node(project, group) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
}
// ---- Track / block state events (B4c) ---------------------------------------
// The caller left one clip at 10..40 on video track 0 (see
// test_sequence_events); `track` is that track.
static void test_track_extra_events(OakEngineSequence *seq,
OakEngineTrack *track)
{
EventLog log;
memset(&log, 0, sizeof(log));
// Family mismatches: a track is not a sequence and vice versa.
assert(oakengine_event_subscribe(
track, OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED,
record_event, &log) == 0);
assert(oakengine_event_subscribe(seq, OAKENGINE_EVENT_TRACK_MUTED_CHANGED,
record_event, &log) == 0);
// Muted changed.
int64_t sub_mute = oakengine_event_subscribe(
track, OAKENGINE_EVENT_TRACK_MUTED_CHANGED, record_event, &log);
assert(sub_mute > 0);
assert(oakengine_track_set_muted(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1) ==
OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_TRACK_MUTED_CHANGED] == 1);
assert(log.last_a[OAKENGINE_EVENT_TRACK_MUTED_CHANGED] == 1);
assert(oakengine_track_set_muted(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0) ==
OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_TRACK_MUTED_CHANGED] == 2);
assert(log.last_a[OAKENGINE_EVENT_TRACK_MUTED_CHANGED] == 0);
assert(oakengine_event_unsubscribe(sub_mute) == OAKENGINE_OK);
// Track height changed (track-level, double bit pattern) and the
// sequence-level pixel variant.
int64_t sub_h = oakengine_event_subscribe(
track, OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED, record_event, &log);
int64_t sub_sh = oakengine_event_subscribe(
seq, OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED, record_event,
&log);
assert(sub_h > 0 && sub_sh > 0);
assert(oakengine_track_set_height(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
2.5) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED] == 1);
double h;
memcpy(&h, &log.last_a[OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED], sizeof(h));
assert(h == 2.5);
assert(log.count[OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED] == 1);
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED] ==
OAKENGINE_TRACK_TYPE_VIDEO);
assert(log.last_b[OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED] ==
oakengine_track_height_internal_to_pixels(2.5));
assert(log.last_handle[OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED] ==
(void *)track);
assert(oakengine_event_unsubscribe(sub_h) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_sh) == OAKENGINE_OK);
// Track list changed + index changed: append a second video track,
// then move track 0 to position 1.
int64_t sub_list = oakengine_event_subscribe(
seq, OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED, record_event, &log);
assert(sub_list > 0);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
1);
assert(log.count[OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED] >= 1);
assert(log.last_a[OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED] ==
OAKENGINE_TRACK_TYPE_VIDEO);
int64_t sub_index = oakengine_event_subscribe(
track, OAKENGINE_EVENT_TRACK_INDEX_CHANGED, record_event, &log);
assert(sub_index > 0);
assert(oakengine_sequence_move_track(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
1) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_TRACK_INDEX_CHANGED] >= 1);
assert(log.last_b[OAKENGINE_EVENT_TRACK_INDEX_CHANGED] == 1);
assert(oakengine_sequence_move_track(seq, OAKENGINE_TRACK_TYPE_VIDEO, 1,
0) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_index) == OAKENGINE_OK);
// Clean up the extra track.
assert(oakengine_sequence_remove_track(seq, OAKENGINE_TRACK_TYPE_VIDEO,
1) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_list) == OAKENGINE_OK);
// Blocks refreshed: emitted when the track re-lays out its block chain
// (e.g. moving a clip onto it).
int64_t sub_refresh = oakengine_event_subscribe(
track, OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED, record_event, &log);
assert(sub_refresh > 0);
assert(oakengine_sequence_move_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0,
50) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED] >= 1);
// Move it back to 10 to restore the original layout.
assert(oakengine_sequence_move_clip(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0,
10) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_refresh) == OAKENGINE_OK);
// Subtitles changed: subscription validates (no headless trigger --
// the signal only fires on subtitle-track cache invalidation).
int64_t sub_subs = oakengine_event_subscribe(
seq, OAKENGINE_EVENT_SEQUENCE_SUBTITLES_CHANGED, record_event, &log);
assert(sub_subs > 0);
assert(oakengine_event_unsubscribe(sub_subs) == OAKENGINE_OK);
}
static void test_block_state_events(OakEngineSequence *seq,
const char *media_path)
{
EventLog log;
memset(&log, 0, sizeof(log));
// The clip is back at 10..40 (clip index 0; the leading gap is not
// counted by the clip family).
OakEngineClip *clip = oakengine_sequence_clip_at(
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
assert(clip != NULL);
OakEngineBlock *block = (OakEngineBlock *)clip;
// Family mismatch: a block is not a track.
assert(oakengine_event_subscribe(
block, OAKENGINE_EVENT_TRACK_MUTED_CHANGED, record_event,
&log) == 0);
int64_t sub_en = oakengine_event_subscribe(
block, OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED, record_event, &log);
assert(sub_en > 0);
// The engine emits enabled_changed twice per flip (Block::set_enabled
// and Block::InputValueChangedEvent), so each toggle delivers two.
OakEngineClip *clips[1] = { clip };
assert(oakengine_clip_toggle_enabled(clips, 1) == 1);
assert(log.count[OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED] == 2);
assert(oakengine_block_is_enabled(block) == 0);
assert(oakengine_clip_toggle_enabled(clips, 1) == 1);
assert(log.count[OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED] == 4);
assert(oakengine_block_is_enabled(block) == 1);
assert(oakengine_event_unsubscribe(sub_en) == OAKENGINE_OK);
// Preview changed: writing the loop-mode input fires it.
int64_t sub_prev = oakengine_event_subscribe(
block, OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED, record_event, &log);
assert(sub_prev > 0);
oak_node_value v;
memset(&v, 0, sizeof(v));
v.type = OAK_NODE_VALUE_COMBO;
v.num = 1;
assert(oakengine_node_set_input((OakEngineNode *)clip,
oakengine_clip_loop_mode_input_id(),
&v) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED] == 1);
v.num = 0;
assert(oakengine_node_set_input((OakEngineNode *)clip,
oakengine_clip_loop_mode_input_id(),
&v) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED] == 2);
assert(oakengine_event_unsubscribe(sub_prev) == OAKENGINE_OK);
// Node links/color changed (Node signals on the block handle).
int64_t sub_links = oakengine_event_subscribe(
(OakEngineNode *)clip, OAKENGINE_EVENT_NODE_LINKS_CHANGED,
record_event, &log);
int64_t sub_color = oakengine_event_subscribe(
(OakEngineNode *)clip, OAKENGINE_EVENT_NODE_COLOR_CHANGED,
record_event, &log);
assert(sub_links > 0 && sub_color > 0);
OakEngineNode *one[1] = { (OakEngineNode *)clip };
assert(oakengine_node_set_color_label(one, 1, 4) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_COLOR_CHANGED] == 1);
OakEngineFootage *footage =
oakengine_project_import_footage(oakengine_node_get_project(
(OakEngineNode *)seq),
media_path);
assert(footage != NULL);
OakEngineClip *clip2 = oakengine_sequence_add_footage_clip(
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 50, 80, 0);
assert(clip2 != NULL);
OakEngineClip *pair[2] = { clip, clip2 };
assert(oakengine_clip_set_linked(pair, 2, 1) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_NODE_LINKS_CHANGED] >= 1);
assert(oakengine_clip_set_linked(pair, 2, 0) == OAKENGINE_OK);
oakengine_footage_free(footage);
assert(oakengine_event_unsubscribe(sub_links) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_color) == OAKENGINE_OK);
}
static void test_marker_list_events(OakEngineSequence *seq)
{
EventLog log;
memset(&log, 0, sizeof(log));
OakEngineMarkerList *list =
oakengine_viewer_get_marker_list((OakEngineNode *)seq);
assert(list != NULL);
// Family mismatch: a marker list is not a workarea.
assert(oakengine_event_subscribe(
list, OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED, record_event,
&log) == 0);
int64_t sub_add = oakengine_event_subscribe(
list, OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED, record_event, &log);
int64_t sub_mod = oakengine_event_subscribe(
list, OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED, record_event,
&log);
int64_t sub_rm = oakengine_event_subscribe(
list, OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED, record_event,
&log);
assert(sub_add > 0 && sub_mod > 0 && sub_rm > 0);
// Add a marker at 2 seconds (rational seconds, not timestamps).
assert(oakengine_marker_list_add(list, 2, 1, 2, 1, "ListMark", 3) ==
OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED] == 1);
OakEngineMarker *marker = (OakEngineMarker *)log.last_handle
[OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED];
assert(marker != NULL);
assert(oakengine_marker_list_at(list, 0) != NULL);
// Modify: recolor through the properties batch.
OakEngineMarker *one[1] = { marker };
assert(oakengine_marker_set_properties(one, 1, 5, NULL, 0, 0, 0, 0, 0,
NULL) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED] == 1);
assert(log.last_handle[OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED] ==
(void *)marker);
// Remove.
assert(oakengine_marker_remove(marker) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED] == 1);
assert(oakengine_event_unsubscribe(sub_add) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_mod) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_rm) == OAKENGINE_OK);
}
static void test_workarea_events(OakEngineSequence *seq)
{
EventLog log;
memset(&log, 0, sizeof(log));
// Viewer-owned (borrowed) workarea.
OakEngineWorkarea *wa =
oakengine_viewer_get_workarea_handle((OakEngineNode *)seq);
assert(wa != NULL);
int64_t sub_range = oakengine_event_subscribe(
wa, OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED, record_event, &log);
int64_t sub_en = oakengine_event_subscribe(
wa, OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED, record_event, &log);
assert(sub_range > 0 && sub_en > 0);
assert(oakengine_workarea_set_range(wa, 1, 1, 4, 1) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED] == 1);
int64_t in_num = 0, in_den = 0, out_num = 0, out_den = 0;
assert(oakengine_workarea_get(wa, &in_num, &in_den, &out_num, &out_den,
NULL) == OAKENGINE_OK);
assert(in_num == 1 && in_den == 1 && out_num == 4 && out_den == 1);
assert(oakengine_workarea_set_enabled(wa, 1) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED] == 1);
assert(log.last_a[OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED] == 1);
assert(oakengine_workarea_set_enabled(wa, 0) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_range) == OAKENGINE_OK);
assert(oakengine_event_unsubscribe(sub_en) == OAKENGINE_OK);
// Standalone owned workarea (the footage viewer override pattern).
OakEngineWorkarea *over = oakengine_workarea_create();
assert(over != NULL);
int64_t sub_over = oakengine_event_subscribe(
over, OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED, record_event, &log);
assert(sub_over > 0);
assert(oakengine_workarea_set_range(over, 0, 1, 7, 2) == OAKENGINE_OK);
assert(log.count[OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED] == 2);
assert(oakengine_event_unsubscribe(sub_over) == OAKENGINE_OK);
oakengine_workarea_free(over);
oakengine_workarea_free(NULL);
}
int main(void)
{
make_tmpdir();
// Sandbox the config/cache/data locations (see oakengine_init_test).
#if !defined(_WIN32)
assert(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0);
assert(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0);
assert(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0);
#endif
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "Events");
assert(seq != NULL);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
0);
char path[4096];
demo_path(path, sizeof(path));
OakEngineTrack *track = oakengine_sequence_track_at(
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0);
assert(track != NULL);
test_subscribe_validation(project, seq, track);
test_project_events(project);
test_folder_events(project);
test_sequence_events(project, seq, path);
test_block_traversal(seq);
test_track_extra_events(seq, track);
test_block_state_events(seq, path);
test_marker_list_events(seq);
test_workarea_events(seq);
test_node_events(project);
test_group_events(project);
oakengine_project_free(project);
assert(oakengine_shutdown() == OAKENGINE_OK);
printf("oakengine_events_test: all assertions passed\n");
return 0;
}
+5
View File
@@ -424,6 +424,10 @@ int main(void)
// ---- render_ex: real exports ---------------------------------------------
{
// H.264 + AAC over a custom 20-frame range of the same sequence.
// Exercise the encoder-specific video option pass-through
// (crf=18) for this render, then clear it so later exports and
// the cancellation re-run are unaffected.
oakengine_export_set_video_option("crf", "18");
char out3[4096];
snprintf(out3, sizeof(out3), "%s/ex_custom.mp4", g_tmpdir);
oak_export_options_ex o3;
@@ -448,6 +452,7 @@ int main(void)
"(no error)");
}
assert(rc == OAKENGINE_OK);
oakengine_export_set_video_option(NULL, NULL);
snprintf(cmd, sizeof(cmd),
"ffprobe -v error -show_entries stream=codec_type,duration "
"-of csv=p=0 \"%s\"",
+292
View File
@@ -38,7 +38,9 @@
#include "oakengine/footage.h"
#include "oakengine/init.h"
#include "oakengine/node.h"
#include "oakengine/project.h"
#include "oakengine/timeline.h"
#ifndef OAK_TEST_SOURCE_DIR
#define OAK_TEST_SOURCE_DIR "."
@@ -588,6 +590,293 @@ static void test_colorspace_candidates(void)
oakengine_project_free(project);
}
// Project extras: filenames, cache paths, settings, MIME type, from_object.
static void test_project_extras(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
char buf[4096];
// Untitled project: pretty filename is the "(untitled)" placeholder.
assert(oakengine_project_pretty_filename(project, buf, sizeof(buf)) > 0);
assert(strlen(buf) > 0);
// set_filename round-trips through the plain filename getter.
char target[4096];
snprintf(target, sizeof(target), "%s/roundtrip.ove", g_tmpdir);
assert(oakengine_project_set_filename(project, target) == OAKENGINE_OK);
assert(oakengine_project_filename(project, buf, sizeof(buf)) > 0);
assert(strcmp(buf, target) == 0);
assert(oakengine_project_set_filename(project, NULL) ==
OAKENGINE_E_INVALID);
assert(oakengine_project_set_filename(NULL, target) ==
OAKENGINE_E_INVALID);
// With a filename set, the cache paths are derivable and non-empty.
assert(oakengine_project_cache_path(project, buf, sizeof(buf)) > 0);
assert(strlen(buf) > 0);
assert(oakengine_project_cache_alongside_path(project, buf, sizeof(buf)) >
0);
assert(strlen(buf) > 0);
// Custom cache path setting round-trip (NULL clears).
assert(oakengine_project_set_custom_cache_path(project, "/tmp/oakcache") ==
OAKENGINE_OK);
assert(oakengine_project_get_custom_cache_path(project, buf,
sizeof(buf)) > 0);
assert(strcmp(buf, "/tmp/oakcache") == 0);
assert(oakengine_project_set_custom_cache_path(project, NULL) ==
OAKENGINE_OK);
assert(oakengine_project_get_custom_cache_path(project, buf,
sizeof(buf)) == 0);
// Color reference space setting round-trip.
assert(oakengine_project_set_color_reference_space(
project, "Rec.709 OETF") == OAKENGINE_OK);
assert(oakengine_project_get_color_reference_space(project, buf,
sizeof(buf)) > 0);
assert(strcmp(buf, "Rec.709 OETF") == 0);
assert(oakengine_project_set_color_reference_space(NULL, "x") ==
OAKENGINE_E_INVALID);
// Cache location setting defaults to a valid enum value; NULL is invalid.
assert(oakengine_project_get_cache_location_setting(project) >= 0);
assert(oakengine_project_get_cache_location_setting(NULL) < 0);
// The project item MIME type is a non-empty static string.
const char *mime = oakengine_project_item_mime_type();
assert(mime != NULL && strlen(mime) > 0);
// from_object: the root node resolves back to its owning project.
OakEngineNode *root = oakengine_project_node_at(project, 0);
assert(root != NULL);
assert(oakengine_project_from_object(root) == project);
assert(oakengine_project_from_object(NULL) == NULL);
// NULL safety.
assert(oakengine_project_pretty_filename(NULL, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_project_cache_path(NULL, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_project_get_custom_cache_path(NULL, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_project_get_color_reference_space(NULL, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
oakengine_project_free(project);
}
// Folder creation and child queries.
static void test_folder(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
// A fresh project's first node is its root folder.
OakEngineNode *root = oakengine_project_node_at(project, 0);
assert(root != NULL);
OakEngineNode *folder =
oakengine_folder_create(project, root, "My Folder");
assert(folder != NULL);
assert(oakengine_folder_has_child_recursive(root, folder) == 1);
assert(oakengine_folder_index_of_child(root, folder) >= 0);
// A subfolder is found recursively from the root.
OakEngineNode *sub = oakengine_folder_create(project, folder, "Sub");
assert(sub != NULL);
assert(oakengine_folder_has_child_recursive(root, sub) == 1);
assert(oakengine_folder_has_child_recursive(folder, sub) == 1);
assert(oakengine_folder_has_child_recursive(sub, folder) == 0);
// A folder from another project is not a child here.
OakEngineProject *other = oakengine_project_create();
assert(other != NULL);
assert(oakengine_project_new(other) == OAKENGINE_OK);
OakEngineNode *other_root = oakengine_project_node_at(other, 0);
assert(other_root != NULL);
OakEngineNode *alien = oakengine_folder_create(other, other_root, "Alien");
assert(alien != NULL);
assert(oakengine_folder_has_child_recursive(root, alien) == 0);
assert(oakengine_folder_index_of_child(root, alien) ==
OAKENGINE_E_NOT_FOUND);
oakengine_project_free(other);
// The child input key is a non-empty static string.
const char *key = oakengine_folder_child_input_key();
assert(key != NULL && strlen(key) > 0);
// Error paths: non-folder parents, non-folder queries, NULL.
assert(oakengine_folder_create(project, folder, NULL) != NULL);
OakEngineNode *footage_node = NULL;
{
char path[4096];
demo_path(path, sizeof(path));
OakEngineFootage *f = oakengine_project_import_footage(project, path);
assert(f != NULL);
oakengine_footage_free(f);
// The imported footage is a non-folder project node.
for (int i = 0; i < oakengine_project_node_count(project); i++) {
OakEngineNode *n = oakengine_project_node_at(project, i);
char id[128];
assert(oakengine_node_get_type_id(n, id, sizeof(id)) > 0);
if (strcmp(id, "org.olivevideoeditor.Olive.folder") != 0) {
footage_node = n;
break;
}
}
assert(footage_node != NULL);
}
assert(oakengine_folder_create(project, footage_node, "Nope") == NULL);
assert(oakengine_folder_has_child_recursive(footage_node, folder) == 0);
assert(oakengine_folder_index_of_child(footage_node, folder) ==
OAKENGINE_E_INVALID);
assert(oakengine_folder_has_child_recursive(NULL, folder) == 0);
assert(oakengine_folder_has_child_recursive(root, NULL) == 0);
assert(oakengine_folder_index_of_child(NULL, folder) ==
OAKENGINE_E_INVALID);
assert(oakengine_folder_index_of_child(root, NULL) == OAKENGINE_E_INVALID);
assert(oakengine_folder_create(NULL, root, "Nope") == NULL);
oakengine_project_free(project);
}
// Footage extras: filename, stream references, descriptions, proxy params,
// manual proxy state and invalidation.
static void test_footage_extras(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
char path[4096];
demo_path(path, sizeof(path));
OakEngineFootage *f = oakengine_project_import_footage(project, path);
assert(f != NULL);
char buf[4096];
// Filename of the imported footage.
assert(oakengine_footage_get_filename(f, buf, sizeof(buf)) > 0);
assert(strcmp(buf, path) == 0);
assert(oakengine_footage_get_filename(NULL, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
// Real stream index 0 is the video stream, 1 the audio stream.
int track_type = -1, stream_index = -1;
assert(oakengine_footage_get_stream_reference(f, 0, &track_type,
&stream_index) == OAKENGINE_OK);
assert(track_type == OAKENGINE_TRACK_TYPE_VIDEO && stream_index == 0);
assert(oakengine_footage_get_stream_reference(f, 1, &track_type,
&stream_index) == OAKENGINE_OK);
assert(track_type == OAKENGINE_TRACK_TYPE_AUDIO && stream_index == 0);
assert(oakengine_footage_get_stream_reference(f, 99, &track_type,
&stream_index) ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_footage_get_stream_reference(NULL, 0, &track_type,
&stream_index) ==
OAKENGINE_E_INVALID);
// Stream descriptions.
assert(oakengine_footage_describe_video_stream(f, 0, buf, sizeof(buf)) >
0);
assert(strlen(buf) > 0);
assert(oakengine_footage_describe_audio_stream(f, 0, buf, sizeof(buf)) >
0);
assert(strlen(buf) > 0);
assert(oakengine_footage_describe_video_stream(f, 9, buf, sizeof(buf)) ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_footage_describe_audio_stream(f, 9, buf, sizeof(buf)) ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_footage_describe_video_stream(NULL, 0, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
// Static stream type names (no handle needed).
assert(oakengine_footage_stream_type_name(OAKENGINE_TRACK_TYPE_VIDEO, buf,
sizeof(buf)) > 0);
assert(strlen(buf) > 0);
assert(oakengine_footage_stream_type_name(OAKENGINE_TRACK_TYPE_AUDIO, buf,
sizeof(buf)) > 0);
assert(strlen(buf) > 0);
// Proxy params: effective defaults first, then a custom round-trip.
assert(oakengine_footage_has_custom_proxy_params(f) == 0);
oak_proxy_params params;
memset(&params, 0, sizeof(params));
assert(oakengine_footage_get_effective_proxy_params(f, &params) ==
OAKENGINE_OK);
assert(params.width > 0 && params.height > 0);
params.width = 640;
params.height = 360;
params.divider = 1;
params.version = 1;
params.crf = 30;
params.include_audio = 0;
strcpy(params.extension, "mkv");
strcpy(params.preset, "slow");
assert(oakengine_footage_set_custom_proxy_params(f, &params) ==
OAKENGINE_OK);
assert(oakengine_footage_has_custom_proxy_params(f) == 1);
oak_proxy_params back;
memset(&back, 0, sizeof(back));
assert(oakengine_footage_get_effective_proxy_params(f, &back) ==
OAKENGINE_OK);
assert(back.width == 640 && back.height == 360 && back.crf == 30);
assert(back.include_audio == 0);
assert(strcmp(back.extension, "mkv") == 0);
assert(strcmp(back.preset, "slow") == 0);
assert(oakengine_footage_clear_custom_proxy_params(f) == OAKENGINE_OK);
assert(oakengine_footage_has_custom_proxy_params(f) == 0);
assert(oakengine_footage_set_custom_proxy_params(f, NULL) ==
OAKENGINE_E_INVALID);
assert(oakengine_footage_get_effective_proxy_params(f, NULL) ==
OAKENGINE_E_INVALID);
assert(oakengine_footage_has_custom_proxy_params(NULL) ==
OAKENGINE_E_INVALID);
// Manual proxy state: set then clear (no file is created here).
assert(oakengine_footage_set_proxy(f, "/tmp/fake_proxy.mp4", 2, 0, 1,
1) == OAKENGINE_OK);
assert(oakengine_footage_proxy_get_state(f) == 2);
assert(oakengine_footage_proxy_get_path(f, buf, sizeof(buf)) > 0);
assert(strcmp(buf, "/tmp/fake_proxy.mp4") == 0);
assert(oakengine_footage_proxy_is_enabled(f) == 1);
assert(oakengine_footage_clear_proxy(f) == OAKENGINE_OK);
assert(oakengine_footage_proxy_get_state(f) == 0);
assert(oakengine_footage_proxy_get_path(f, buf, sizeof(buf)) == 0);
assert(oakengine_footage_set_proxy(NULL, "x", 2, 0, 1, 1) ==
OAKENGINE_E_INVALID);
assert(oakengine_footage_clear_proxy(NULL) == OAKENGINE_E_INVALID);
// Cache invalidation after proxy/relink changes.
assert(oakengine_footage_invalidate(f) == OAKENGINE_OK);
assert(oakengine_footage_invalidate(NULL) == OAKENGINE_E_INVALID);
// Probe handles carry no project node: the whole section rejects them.
OakEngineFootage *probed = oakengine_footage_probe(path);
assert(probed != NULL);
assert(oakengine_footage_get_filename(probed, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_footage_get_stream_reference(probed, 0, &track_type,
&stream_index) ==
OAKENGINE_E_INVALID);
assert(oakengine_footage_describe_video_stream(probed, 0, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_footage_get_effective_proxy_params(probed, &params) ==
OAKENGINE_E_INVALID);
assert(oakengine_footage_invalidate(probed) == OAKENGINE_E_INVALID);
oakengine_footage_free(probed);
oakengine_footage_free(f);
oakengine_project_free(project);
}
int main(void)
{
make_tmpdir();
@@ -610,6 +899,9 @@ int main(void)
test_proxy();
test_stream_overrides();
test_colorspace_candidates();
test_project_extras();
test_folder();
test_footage_extras();
assert(oakengine_shutdown() == OAKENGINE_OK);
+260
View File
@@ -562,6 +562,265 @@ static void test_keyframe_properties(OakEngineProject *project,
0.f) == OAKENGINE_E_INVALID);
}
// Handle-based keyframe family (B8a): enumeration, navigation, handle
// accessors, live mutation, undoable batch operations, detached
// create/paste/dispose and the input dragger.
static void test_handle_family(OakEngineProject *project,
OakEngineNode *opacity)
{
char buf[256];
oak_node_value v;
// Start from a clean, keyframing-enabled, empty input.
assert(oakengine_node_keyframes_clear(opacity, "opacity_in") ==
OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 0);
// Toggle ON at 1s: one keyframe with the current value and best type.
assert(oakengine_node_keyframes_toggle_at_time(
opacity, "opacity_in", -1, 1, 1, 1, NULL) == OAKENGINE_OK);
assert(oakengine_node_keyframe_track_count(opacity, "opacity_in", -1) ==
1);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 1);
assert(oakengine_node_has_keyframe_at_time(opacity, "opacity_in", -1, 1,
1) == 1);
// Toggling on again at the same time is a no-op.
assert(oakengine_node_keyframes_toggle_at_time(
opacity, "opacity_in", -1, 1, 1, 1, NULL) == OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 1);
// More keys via toggles for navigation tests.
assert(oakengine_node_keyframes_toggle_at_time(
opacity, "opacity_in", -1, 0, 1, 1, NULL) == OAKENGINE_OK);
assert(oakengine_node_keyframes_toggle_at_time(
opacity, "opacity_in", -1, 3, 1, 1, NULL) == OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 3);
// Navigation.
int64_t num = -1, den = -1;
assert(oakengine_node_keyframe_earliest_time(opacity, "opacity_in", -1,
&num, &den) == 1);
assert(num == 0 && den == 1);
assert(oakengine_node_keyframe_latest_time(opacity, "opacity_in", -1,
&num, &den) == 1);
assert(num == 3 && den == 1);
assert(oakengine_node_keyframe_closest_time_before(
opacity, "opacity_in", -1, 2, 1, &num, &den) == 1);
assert(num == 1 && den == 1);
assert(oakengine_node_keyframe_closest_time_after(
opacity, "opacity_in", -1, 2, 1, &num, &den) == 1);
assert(num == 3 && den == 1);
assert(oakengine_node_keyframe_closest_time_before(
opacity, "opacity_in", -1, 0, 1, &num, &den) == 0);
assert(oakengine_node_keyframe_closest_time_after(
opacity, "opacity_in", -1, 3, 1, &num, &den) == 0);
// Handle lookup: on-track enumeration, at-time lookup, and the batch
// at-time query all agree.
OakEngineKeyframe *k0 =
oakengine_node_keyframe_handle_on_track(opacity, "opacity_in", -1, 0, 0);
OakEngineKeyframe *k1 =
oakengine_node_keyframe_handle_on_track(opacity, "opacity_in", -1, 0, 1);
assert(k0 != NULL && k1 != NULL && k0 != k1);
assert(oakengine_node_keyframe_handle_on_track(opacity, "opacity_in", -1,
0, 3) == NULL);
assert(oakengine_node_keyframe_handle_on_track(opacity, "opacity_in", -1,
1, 0) == NULL);
assert(oakengine_node_keyframe_handle_at_time(opacity, "opacity_in", -1,
0, 0, 1) == k0);
assert(oakengine_node_keyframe_handle_at_time(opacity, "opacity_in", -1,
0, 1, 1) == k1);
assert(oakengine_node_keyframe_handle_at_time(opacity, "opacity_in", -1,
0, 2, 1) == NULL);
OakEngineKeyframe *at[4] = { NULL, NULL, NULL, NULL };
assert(oakengine_node_keyframes_at_time(opacity, "opacity_in", -1, 1, 1,
at, 4) == 1);
assert(at[0] == k1);
// Handle accessors.
assert(oakengine_keyframe_get_time(k1, &num, &den) == OAKENGINE_OK);
assert(num == 1 && den == 1);
assert(oakengine_keyframe_get_input_id(k1, buf, sizeof(buf)) > 0);
assert(strcmp(buf, "opacity_in") == 0);
assert(oakengine_keyframe_get_track(k1) == 0);
assert(oakengine_keyframe_get_element(k1) == -1);
assert(oakengine_keyframe_get_node(k1) == opacity);
assert(oakengine_keyframe_get_type(k1) >= 0);
assert(oakengine_keyframe_default_type() >= 0);
assert(oakengine_keyframe_get_value(k1, &v) == OAKENGINE_OK);
assert(v.type == OAK_NODE_VALUE_FLOAT);
// Sibling check: a key at 0s sees the key at 1s and vice versa.
assert(oakengine_keyframe_has_sibling_at_time(k0, 1, 1) == 1);
assert(oakengine_keyframe_has_sibling_at_time(k0, 0, 1) == 0);
// NULL safety.
assert(oakengine_keyframe_get_time(NULL, &num, &den) ==
OAKENGINE_E_INVALID);
assert(oakengine_keyframe_get_type(NULL) == -1);
assert(oakengine_keyframe_get_node(NULL) == NULL);
assert(oakengine_keyframe_get_track(NULL) == -1);
assert(oakengine_keyframe_has_sibling_at_time(NULL, 1, 1) == 0);
// Bezier points: set easing through the existing API, then live-move a
// handle (no undo entry) and read it back raw and valid.
assert(oakengine_node_keyframe_add(opacity, "opacity_in", 45, &v, 1,
0.1f, 0.2f, 0.3f,
0.4f) == OAKENGINE_OK);
assert(oakengine_keyframe_set_bezier_point_live(k1, 0, 0.11, 0.22) ==
OAKENGINE_OK);
double x = 0, y = 0;
assert(oakengine_keyframe_get_bezier_point(k1, 0, &x, &y) ==
OAKENGINE_OK);
assert(fabs(x - 0.11) < 1e-9 && fabs(y - 0.22) < 1e-9);
assert(oakengine_keyframe_get_valid_bezier_point(k1, 0, &x, &y) ==
OAKENGINE_OK);
assert(oakengine_keyframe_get_bezier_point(k1, 2, &x, &y) ==
OAKENGINE_E_INVALID);
// The live move pushed no undo entry of its own: undoing pops the add.
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 3);
// Live value/time mutation.
assert(oakengine_keyframe_set_value_live(k1, &v) == OAKENGINE_OK);
oak_node_value readback;
assert(oakengine_keyframe_get_value(k1, &readback) == OAKENGINE_OK);
assert(fabs(readback.f[0] - v.f[0]) < 1e-9);
assert(oakengine_keyframe_set_time_live(k1, 2, 1) == OAKENGINE_OK);
assert(oakengine_node_has_keyframe_at_time(opacity, "opacity_in", -1, 2,
1) == 1);
assert(oakengine_keyframe_set_time_live(k1, 1, 1) == OAKENGINE_OK);
// remove_many: delete the keys at 0s and 3s as ONE undoable command.
OakEngineKeyframe *victims[2] = { k0, oakengine_node_keyframe_handle_at_time(
opacity, "opacity_in", -1, 0, 3,
1) };
assert(victims[1] != NULL);
assert(oakengine_keyframes_remove_many(victims, 2, NULL) == OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 1);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 3);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 1);
// NULL entries are refused and nothing is pushed.
OakEngineKeyframe *with_null[2] = { k1, NULL };
assert(oakengine_keyframes_remove_many(with_null, 2, NULL) ==
OAKENGINE_E_INVALID);
// Detached create + paste as ONE undoable command, then dispose.
memset(&v, 0, sizeof(v));
v.type = OAK_NODE_VALUE_FLOAT;
v.f[0] = 0.33;
OakEngineKeyframe *detached1 = oakengine_keyframe_create(
opacity, "opacity_in", -1, 0, 5, 1, &v, 0);
OakEngineKeyframe *detached2 = oakengine_keyframe_create(
opacity, "opacity_in", -1, 0, 6, 1, &v, 0);
OakEngineKeyframe *detached3 = oakengine_keyframe_create(
opacity, "opacity_in", -1, 0, 7, 1, &v, 0);
assert(detached1 != NULL && detached2 != NULL && detached3 != NULL);
assert(oakengine_keyframe_create(opacity, "no_such", -1, 0, 5, 1, &v,
0) == NULL);
v.f[0] = 1.5;
OakEngineKeyframe *both[2] = { detached1, detached2 };
assert(oakengine_node_keyframes_paste(opacity, both, 2, NULL) ==
OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 3);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 1);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 3);
oakengine_keyframe_dispose(detached3);
oakengine_keyframe_dispose(NULL); // no-op
// Toggle OFF the key at 1s: removed, single-track standard value fix-up.
assert(oakengine_node_keyframes_toggle_at_time(
opacity, "opacity_in", -1, 1, 1, 0, NULL) == OAKENGINE_OK);
assert(oakengine_node_has_keyframe_at_time(opacity, "opacity_in", -1, 1,
1) == 0);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_has_keyframe_at_time(opacity, "opacity_in", -1, 1,
1) == 1);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
// Disable keyframing entirely: all keys gone, keyframing flag off.
assert(oakengine_node_set_input_keyframing(opacity, "opacity_in", -1, 0,
1, 1, NULL) == OAKENGINE_OK);
assert(oakengine_node_input_is_keyframed(opacity, "opacity_in") == 0);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 0);
// Re-enable through the facade: one default-type key per track.
assert(oakengine_node_set_input_keyframing(opacity, "opacity_in", -1, 1,
1, 1, NULL) == OAKENGINE_OK);
assert(oakengine_node_input_is_keyframed_ex(opacity, "opacity_in", -1) ==
1);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 1);
OakEngineKeyframe *sole = oakengine_node_keyframe_handle_on_track(
opacity, "opacity_in", -1, 0, 0);
assert(oakengine_keyframe_get_type(sole) ==
oakengine_keyframe_default_type());
// Redundant enable is a no-op success.
assert(oakengine_node_set_input_keyframing(opacity, "opacity_in", -1, 1,
1, 1, NULL) == OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == 1);
// Undo both steps back to keyframing disabled, then redo to enabled.
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_input_is_keyframed(opacity, "opacity_in") == 0);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_node_input_is_keyframed(opacity, "opacity_in") == 1);
// Input dragger: start creates a key at the drag time, drag live-sets,
// end pushes ONE undoable command.
OakEngineNodeDragger *dragger =
oakengine_dragger_create(opacity, "opacity_in", -1, 0);
assert(dragger != NULL);
assert(oakengine_dragger_create(opacity, "no_such", -1, 0) == NULL);
assert(oakengine_dragger_is_started(dragger) == 0);
assert(oakengine_dragger_end(dragger, NULL) == OAKENGINE_E_STATE);
const int keys_before =
oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1, 0);
assert(oakengine_dragger_start(dragger, 4, 1, 1) == OAKENGINE_OK);
assert(oakengine_dragger_is_started(dragger) == 1);
assert(oakengine_dragger_start(dragger, 4, 1, 1) == OAKENGINE_E_STATE);
oak_node_value drag_value;
memset(&drag_value, 0, sizeof(drag_value));
drag_value.type = OAK_NODE_VALUE_FLOAT;
drag_value.f[0] = 0.9;
assert(oakengine_dragger_drag(dragger, &drag_value) == OAKENGINE_OK);
oak_node_value at_time;
assert(oakengine_node_get_input_at_time(opacity, "opacity_in", -1, 0, 4,
1, &at_time) == OAKENGINE_OK);
assert(fabs(at_time.f[0] - 0.9) < 1e-9);
assert(oakengine_dragger_end(dragger, "Drag Opacity") == OAKENGINE_OK);
assert(oakengine_dragger_is_started(dragger) == 0);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) ==
keys_before + 1);
// The whole drag (created key + value) unwinds with one undo.
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_keyframe_count_on_track(opacity, "opacity_in", -1,
0) == keys_before);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
oakengine_dragger_free(dragger);
oakengine_dragger_free(NULL);
// Clean up for later tests.
assert(oakengine_node_keyframes_clear(opacity, "opacity_in") ==
OAKENGINE_OK);
}
int main(void)
{
make_tmpdir();
@@ -597,6 +856,7 @@ int main(void)
test_rational_and_color(project, timeremap, solid);
test_panel_paths(project, opacity, solid);
test_keyframe_properties(project, opacity);
test_handle_family(project, opacity);
oakengine_project_free(project);
assert(oakengine_shutdown() == OAKENGINE_OK);
+69
View File
@@ -0,0 +1,69 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI tests for the liboakengine LUT library facade (oakengine/lut.h).
// Runs headless; no GPU required.
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "oakengine/init.h"
#include "oakengine/lut.h"
static void test_counts_after_init(void)
{
assert(oakengine_lut_directory_count() >= 0);
assert(oakengine_lut_file_count() >= 0);
// Out-of-range index returns an error.
char buf[256];
assert(oakengine_lut_directory_at(-1, buf, sizeof(buf)) < 0);
assert(oakengine_lut_file_at(-1, buf, sizeof(buf)) < 0);
}
static void test_set_directories_round_trip(void)
{
const char *dirs[] = { "/tmp/oak_lut_a", "/tmp/oak_lut_b" };
assert(oakengine_lut_set_directories(dirs, 2) == OAKENGINE_OK);
assert(oakengine_lut_directory_count() == 2);
char buf[256];
assert(oakengine_lut_directory_at(0, buf, sizeof(buf)) > 0);
assert(strcmp(buf, "/tmp/oak_lut_a") == 0);
assert(oakengine_lut_directory_at(1, buf, sizeof(buf)) > 0);
assert(strcmp(buf, "/tmp/oak_lut_b") == 0);
// Clearing the library.
assert(oakengine_lut_set_directories(NULL, 0) == OAKENGINE_OK);
assert(oakengine_lut_directory_count() == 0);
}
int main(void)
{
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
test_counts_after_init();
test_set_directories_round_trip();
assert(oakengine_shutdown() == OAKENGINE_OK);
return 0;
}
+681
View File
@@ -41,6 +41,7 @@
#include "oakengine/node.h"
#include "oakengine/project.h"
#include "oakengine/timeline.h"
#include "oakengine/undo.h"
#ifndef OAK_TEST_SOURCE_DIR
#define OAK_TEST_SOURCE_DIR "."
@@ -294,6 +295,24 @@ static void test_edges(OakEngineProject *project, OakEngineNode *solid,
assert(oakengine_node_disconnect(lut, "tex_in") ==
OAKENGINE_E_NOT_FOUND);
// disconnect_ex with element -1 mirrors disconnect(); on an unconnected
// input it reports E_NOT_FOUND. NULL/unknown-input rejection matches
// disconnect() too.
assert(oakengine_node_disconnect_ex(NULL, "tex_in", -1) ==
OAKENGINE_E_INVALID);
assert(oakengine_node_disconnect_ex(lut, "no_such_input", -1) ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_node_disconnect_ex(lut, "tex_in", -1) ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_node_connect(solid, lut, "tex_in") == OAKENGINE_OK);
assert(oakengine_node_disconnect_ex(lut, "tex_in", -1) == OAKENGINE_OK);
assert(oakengine_node_input_is_connected(lut, "tex_in") == 0);
// Undo the disconnect_ex so the undo/redo sequence below starts from
// the same "connected" state as before this block.
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_node_input_is_connected(lut, "tex_in") == 1);
assert(oakengine_node_disconnect(lut, "tex_in") == OAKENGINE_OK);
// Undo/redo the disconnect and the connect: undo brings the connection
// back, undo again removes it; redoing both replays connect then
// disconnect, so the end state is disconnected.
@@ -369,6 +388,658 @@ static void test_label_and_color_many(OakEngineProject *project)
OAKENGINE_E_INVALID);
}
// Extended metadata and value-at-time family (B8a): input introspection,
// properties, label/input names, defaults, project/edge lookup,
// copy_inputs and the at-time value readers.
static void test_extended_metadata(OakEngineProject *project)
{
char buf[256];
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
OakEngineNode *lut = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.ociolut");
OakEngineNode *text = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.text3");
assert(solid != NULL && lut != NULL && text != NULL);
// Introspection.
assert(oakengine_node_input_is_array(text, "args_in") == 1);
assert(oakengine_node_input_is_array(solid, "color_in") == 0);
assert(oakengine_node_input_array_size(text, "args_in") >= 0);
assert(oakengine_node_input_array_size(solid, "color_in") == 0);
assert(oakengine_node_input_get_flags(solid, "color_in") >= 0);
assert(oakengine_node_input_get_flags(NULL, "color_in") == 0);
assert(oakengine_node_input_is_connectable(lut, "tex_in") == 1);
assert(oakengine_node_input_is_connectable(lut, "lut_file_in") == 0);
assert(oakengine_node_input_is_keyframable(solid, "color_in") == 1);
assert(oakengine_node_input_is_keyframable(lut, "tex_in") == 0);
assert(oakengine_node_input_is_keyframed_ex(solid, "color_in", -1) == 0);
// Properties: set (with and without notification), read back through
// every typed getter, enumerate.
assert(oakengine_node_input_has_property(solid, "color_in",
"my_prop") == 0);
assert(oakengine_node_set_input_property_string(
solid, "color_in", "my_prop", "2.5", 1) == OAKENGINE_OK);
assert(oakengine_node_input_has_property(solid, "color_in",
"my_prop") == 1);
assert(oakengine_node_input_get_property_string(
solid, "color_in", "my_prop", buf, sizeof(buf)) > 0);
assert(strcmp(buf, "2.5") == 0);
double d = 0;
assert(oakengine_node_input_get_property_number(solid, "color_in",
"my_prop", -1, &d) ==
OAKENGINE_OK);
assert(fabs(d - 2.5) < 1e-9);
// The per-track variant resolves (component value is type-dependent).
assert(oakengine_node_input_get_property_number(solid, "color_in",
"my_prop", 2, &d) ==
OAKENGINE_OK);
assert(oakengine_node_set_input_property_string(
solid, "color_in", "int_prop", "7", 1) == OAKENGINE_OK);
int64_t i64 = 0;
assert(oakengine_node_input_get_property_int(solid, "color_in",
"int_prop", &i64) ==
OAKENGINE_OK);
assert(i64 == 7);
assert(oakengine_node_input_get_property_rational(
solid, "color_in", "my_prop", NULL, NULL) == OAKENGINE_OK);
assert(oakengine_node_input_get_property_count(solid, "color_in") >= 1);
assert(oakengine_node_input_get_property_string(
solid, "color_in", "no_such", buf, sizeof(buf)) ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_node_input_get_property_number(solid, "color_in",
"no_such", -1, &d) ==
OAKENGINE_E_NOT_FOUND);
// A scalar string reads back as a one-element list.
assert(oakengine_node_input_get_property_string_list_count(
solid, "color_in", "my_prop") == 1);
assert(oakengine_node_input_get_property_string_list(
solid, "color_in", "my_prop", 0, buf, sizeof(buf)) > 0);
assert(strcmp(buf, "2.5") == 0);
assert(oakengine_node_input_get_property_string_list(
solid, "color_in", "my_prop", 1, buf, sizeof(buf)) ==
OAKENGINE_E_NOT_FOUND);
// Suppressed write keeps the value too.
assert(oakengine_node_set_input_property_string(
solid, "color_in", "my_prop", "3.5", 0) == OAKENGINE_OK);
assert(oakengine_node_input_get_property_string(
solid, "color_in", "my_prop", buf, sizeof(buf)) > 0);
assert(strcmp(buf, "3.5") == 0);
// Names.
assert(oakengine_node_get_label_and_name(solid, buf, sizeof(buf)) > 0);
assert(strcmp(buf, "Solid") == 0);
assert(oakengine_node_set_label(solid, "MySolid") == OAKENGINE_OK);
assert(oakengine_node_get_label_and_name(solid, buf, sizeof(buf)) > 0);
assert(strstr(buf, "MySolid") != NULL && strstr(buf, "Solid") != NULL);
assert(oakengine_node_get_input_name(solid, "color_in", buf,
sizeof(buf)) >= 0);
// Default value: Solid's color defaults to opaque red.
oak_node_value def;
assert(oakengine_node_input_get_default_value(solid, "color_in", 0,
&def) == OAKENGINE_OK);
assert(def.type == OAK_NODE_VALUE_COLOR && fabs(def.f[0] - 1.0) < 1e-6);
assert(oakengine_node_input_get_default_value(solid, "color_in", 99,
&def) == OAKENGINE_E_NOT_FOUND);
// Project and edge lookup.
assert(oakengine_node_get_project(solid) == project);
assert(oakengine_node_get_project(NULL) == NULL);
assert(oakengine_node_input_get_connected_node(lut, "tex_in", -1) ==
NULL);
assert(oakengine_node_connect(solid, lut, "tex_in") == OAKENGINE_OK);
assert(oakengine_node_input_get_connected_node(lut, "tex_in", -1) ==
solid);
assert(oakengine_node_disconnect(lut, "tex_in") == OAKENGINE_OK);
// copy_inputs: values (not connections) transfer as one undoable step.
OakEngineNode *solid2 = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid2 != NULL);
oak_node_value c;
memset(&c, 0, sizeof(c));
c.type = OAK_NODE_VALUE_COLOR;
c.f[0] = 0.1;
c.f[1] = 0.2;
c.f[2] = 0.3;
c.f[3] = 1.0;
assert(oakengine_node_set_input(solid, "color_in", &c) == OAKENGINE_OK);
assert(oakengine_node_copy_inputs(solid2, solid) == OAKENGINE_OK);
assert(oakengine_node_get_input(solid2, "color_in", &def) == OAKENGINE_OK);
assert(fabs(def.f[0] - 0.1) < 1e-6 && fabs(def.f[2] - 0.3) < 1e-6);
assert(oakengine_node_copy_inputs(NULL, solid) == OAKENGINE_E_INVALID);
// At-time readers: whole value and per-track component.
oak_node_value at;
assert(oakengine_node_get_input_at_time(solid, "color_in", -1, -1, 0, 1,
&at) == OAKENGINE_OK);
assert(at.type == OAK_NODE_VALUE_COLOR && fabs(at.f[0] - 0.1) < 1e-6);
assert(oakengine_node_get_input_at_time(solid, "color_in", -1, 2, 0, 1,
&at) == OAKENGINE_OK);
assert(at.type == OAK_NODE_VALUE_COLOR && fabs(at.f[0] - 0.3) < 1e-6);
assert(oakengine_node_get_input_at_time(solid, "enabled_in", -1, 0, 0,
1, &at) == OAKENGINE_OK);
assert(at.type == OAK_NODE_VALUE_BOOL && at.num == 1);
// String-family inputs need the string getter.
assert(oakengine_node_get_input_at_time(text, "text_in", -1, 0, 0, 1,
&at) == OAKENGINE_E_INVALID);
assert(oakengine_node_set_input_string_at_time(text, "text_in", -1, 0,
"hello") == OAKENGINE_OK);
assert(oakengine_node_get_input_string_at_time(text, "text_in", -1, 0,
1, buf,
sizeof(buf)) > 0);
assert(strcmp(buf, "hello") == 0);
// Bezier/binary getters reject mismatched inputs.
double b6[6];
assert(oakengine_node_get_input_bezier_at_time(solid, "color_in", -1, 0,
1, b6) ==
OAKENGINE_E_INVALID);
assert(oakengine_node_get_input_binary_at_time(solid, "color_in", -1, 0,
1, NULL, 0) ==
OAKENGINE_E_INVALID);
// Clean up the played-with nodes so later tests see a fresh graph.
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, solid2) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, text) == OAKENGINE_OK);
}
// ---- Context positions -----------------------------------------------------
static void test_context_positions(OakEngineProject *project)
{
OakEngineNode *group = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.group");
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
OakEngineNode *lut = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.ociolut");
assert(group != NULL && solid != NULL && lut != NULL);
double x = 0, y = 0;
int expanded = -1;
// NULL safety.
assert(oakengine_node_context_contains_node(NULL, solid) ==
OAKENGINE_E_INVALID);
assert(oakengine_node_context_node_count(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_node_context_node_at(NULL, 0, NULL, NULL, NULL) == NULL);
assert(oakengine_node_set_context_position(NULL, solid, 0, 0) ==
OAKENGINE_E_INVALID);
assert(oakengine_node_set_context_expanded(NULL, solid, 1) ==
OAKENGINE_E_INVALID);
// A fresh group context is empty.
assert(oakengine_node_context_contains_node(group, solid) == 0);
assert(oakengine_node_context_node_count(group) == 0);
assert(oakengine_node_get_context_position(group, solid, &x, &y,
&expanded) ==
OAKENGINE_E_NOT_FOUND);
// set_context_position inserts like the C++ setter.
assert(oakengine_node_set_context_position(group, solid, 3.5, -2.0) ==
OAKENGINE_OK);
assert(oakengine_node_context_contains_node(group, solid) == 1);
assert(oakengine_node_context_node_count(group) == 1);
assert(oakengine_node_get_context_position(group, solid, &x, &y,
&expanded) == OAKENGINE_OK);
assert(x == 3.5 && y == -2.0 && expanded == 0);
// Expanded flag round-trips.
assert(oakengine_node_set_context_expanded(group, solid, 1) ==
OAKENGINE_OK);
assert(oakengine_node_get_context_position(group, solid, &x, &y,
&expanded) == OAKENGINE_OK);
assert(expanded == 1);
// Moving keeps the expanded flag.
assert(oakengine_node_set_context_position(group, solid, 1.0, 2.0) ==
OAKENGINE_OK);
assert(oakengine_node_get_context_position(group, solid, &x, &y,
&expanded) == OAKENGINE_OK);
assert(x == 1.0 && y == 2.0 && expanded == 1);
assert(oakengine_node_set_context_position(group, lut, -4.0, 5.0) ==
OAKENGINE_OK);
assert(oakengine_node_context_node_count(group) == 2);
// Enumeration (order is the hash map's; find both by handle).
OakEngineNode *seen0 = oakengine_node_context_node_at(group, 0, &x, &y,
&expanded);
OakEngineNode *seen1 = oakengine_node_context_node_at(group, 1, NULL,
NULL, NULL);
assert(seen0 != NULL && seen1 != NULL && seen0 != seen1);
assert((seen0 == solid || seen0 == lut) &&
(seen1 == solid || seen1 == lut));
assert(oakengine_node_context_node_at(group, 2, NULL, NULL, NULL) ==
NULL);
assert(oakengine_node_context_node_at(group, -1, NULL, NULL, NULL) ==
NULL);
assert(oakengine_project_remove_node(project, group) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
}
// ---- Effect input ------------------------------------------------------------
static void test_get_effect_input(OakEngineProject *project)
{
OakEngineNode *lut = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.ociolut");
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(lut != NULL && solid != NULL);
char buf[64];
int element = 99;
assert(oakengine_node_get_effect_input(NULL, buf, sizeof(buf),
&element) == OAKENGINE_E_INVALID);
// OCIO LUT declares its texture input as the effect input.
assert(oakengine_node_get_effect_input(lut, buf, sizeof(buf),
&element) >= 0);
assert(strcmp(buf, "tex_in") == 0 && element == -1);
// The solid generator has no effect input.
assert(oakengine_node_get_effect_input(solid, buf, sizeof(buf),
&element) ==
OAKENGINE_E_NOT_FOUND);
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
}
// ---- Group nodes -------------------------------------------------------------
static void test_group(OakEngineProject *project)
{
OakEngineNode *group = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.group");
OakEngineNode *group2 = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.group");
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
OakEngineNode *lut = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.ociolut");
assert(group != NULL && group2 != NULL && solid != NULL && lut != NULL);
// Type probe.
assert(oakengine_node_is_group(group) == 1);
assert(oakengine_node_is_group(solid) == 0);
assert(oakengine_node_is_group(NULL) == 0);
assert(oakengine_group_input_passthrough_count(solid) ==
OAKENGINE_E_INVALID);
assert(oakengine_group_add_input_passthrough(solid, lut, "x", -1, NULL,
NULL, 0) ==
OAKENGINE_E_INVALID);
// Direct passthrough add: the generated id is returned. The group must
// contain the inner node first (NodeGroup::add_input_passthrough
// asserts context membership).
assert(oakengine_node_set_context_position(group, solid, 0, 0) ==
OAKENGINE_OK);
char idbuf[64];
assert(oakengine_group_add_input_passthrough(group, solid, "color_in",
-1, NULL, idbuf,
sizeof(idbuf)) > 0);
assert(idbuf[0] != '\0');
assert(oakengine_group_input_passthrough_count(group) == 1);
// Read back the passthrough.
char id_at[64], input_at[64];
OakEngineNode *node_at = NULL;
int element_at = 99;
assert(oakengine_group_input_passthrough_at(group, 0, id_at,
sizeof(id_at), &node_at,
input_at, sizeof(input_at),
&element_at) > 0);
assert(strcmp(id_at, idbuf) == 0 && node_at == solid &&
strcmp(input_at, "color_in") == 0 && element_at == -1);
assert(oakengine_group_input_passthrough_at(group, 1, NULL, 0, NULL,
NULL, 0, NULL) ==
OAKENGINE_E_INVALID);
// Id lookup by (node, input, element).
char idq[64];
assert(oakengine_group_get_id_of_passthrough(group, solid, "color_in",
-1, idq, sizeof(idq)) > 0);
assert(strcmp(idq, idbuf) == 0);
assert(oakengine_group_get_id_of_passthrough(group, lut, "tex_in", -1,
idq, sizeof(idq)) ==
OAKENGINE_E_NOT_FOUND);
// Output passthrough (direct variant).
assert(oakengine_group_get_output_passthrough(group) == NULL);
assert(oakengine_group_set_output_passthrough(group, solid) ==
OAKENGINE_OK);
assert(oakengine_group_get_output_passthrough(group) == solid);
// Resolve one level: (group, idbuf) -> (solid, color_in).
OakEngineNode *resolved_node = NULL;
char resolved_input[64];
int resolved_element = 99;
assert(oakengine_group_resolve_input(group, idbuf, -1, &resolved_node,
resolved_input,
sizeof(resolved_input),
&resolved_element) >= 0);
assert(resolved_node == solid && strcmp(resolved_input, "color_in") == 0);
// Resolving a plain node input passes through unchanged.
assert(oakengine_group_resolve_input(solid, "color_in", -1,
&resolved_node, resolved_input,
sizeof(resolved_input),
&resolved_element) >= 0);
assert(resolved_node == solid && strcmp(resolved_input, "color_in") == 0);
// Nested groups resolve to the innermost real input.
char id2[64];
assert(oakengine_node_set_context_position(group2, group, 0, 0) ==
OAKENGINE_OK);
assert(oakengine_group_add_input_passthrough(group2, group, idbuf, -1,
NULL, id2, sizeof(id2)) > 0);
assert(oakengine_group_resolve_input(group2, id2, -1, &resolved_node,
resolved_input,
sizeof(resolved_input),
&resolved_element) >= 0);
assert(resolved_node == solid && strcmp(resolved_input, "color_in") == 0);
// Direct remove.
assert(oakengine_group_remove_input_passthrough(group, solid, "color_in",
-1) == OAKENGINE_OK);
assert(oakengine_group_input_passthrough_count(group) == 0);
assert(oakengine_group_remove_input_passthrough(group, solid, "color_in",
-1) ==
OAKENGINE_E_NOT_FOUND);
// Undoable add: one command on the project undo stack.
assert(oakengine_node_set_context_position(group, lut, 0, 0) ==
OAKENGINE_OK);
assert(oakengine_group_add_input_passthrough_undoable(group, lut,
"tex_in", -1,
NULL) ==
OAKENGINE_OK);
assert(oakengine_group_input_passthrough_count(group) == 1);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_group_input_passthrough_count(group) == 0);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_group_input_passthrough_count(group) == 1);
// Undoable output passthrough.
assert(oakengine_group_set_output_passthrough_undoable(group, lut) ==
OAKENGINE_OK);
assert(oakengine_group_get_output_passthrough(group) == lut);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_group_get_output_passthrough(group) == solid);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_group_get_output_passthrough(group) == lut);
assert(oakengine_project_remove_node(project, group) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, group2) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
}
// ---- Multi-camera nodes --------------------------------------------------------
static void test_multicam(OakEngineProject *project)
{
OakEngineNode *cam = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.multicam");
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(cam != NULL && solid != NULL);
// Type probe.
assert(oakengine_node_is_multicam(cam) == 1);
assert(oakengine_node_is_multicam(solid) == 0);
assert(oakengine_node_is_multicam(NULL) == 0);
// Input id constants.
const char *cur = oakengine_multicam_input_current();
const char *src = oakengine_multicam_input_sources();
const char *seq = oakengine_multicam_input_sequence();
const char *seqt = oakengine_multicam_input_sequence_type();
assert(cur != NULL && src != NULL && seq != NULL && seqt != NULL);
assert(strcmp(cur, "current_in") == 0);
assert(strcmp(src, "sources_in") == 0);
assert(strcmp(seq, "sequence_in") == 0);
assert(strcmp(seqt, "sequence_type_in") == 0);
// A fresh multicam has no connected sources.
assert(oakengine_multicam_get_source_count(cam) == 0);
assert(oakengine_multicam_get_source_count(solid) ==
OAKENGINE_E_INVALID);
// Grid layout math (static, no node needed).
int rows = 0, cols = 0;
assert(oakengine_multicam_get_rows_and_columns(-1, &rows, &cols) ==
OAKENGINE_E_INVALID);
assert(oakengine_multicam_get_rows_and_columns(1, &rows, &cols) ==
OAKENGINE_OK);
assert(rows == 1 && cols == 1);
assert(oakengine_multicam_get_rows_and_columns(2, &rows, &cols) ==
OAKENGINE_OK);
assert(rows == 1 && cols == 2);
assert(oakengine_multicam_get_rows_and_columns(4, &rows, &cols) ==
OAKENGINE_OK);
assert(rows == 2 && cols == 2);
assert(oakengine_multicam_get_rows_and_columns(5, &rows, &cols) ==
OAKENGINE_OK);
assert(rows == 2 && cols == 3);
// index <-> (row, col) is an inverse pair for every tile.
for (int sources = 1; sources <= 9; sources++) {
assert(oakengine_multicam_get_rows_and_columns(sources, &rows,
&cols) ==
OAKENGINE_OK);
for (int index = 0; index < sources; index++) {
int row = -1, col = -1;
assert(oakengine_multicam_index_to_row_cols(index, rows, cols,
&row, &col) ==
OAKENGINE_OK);
assert(row >= 0 && row < rows && col >= 0 && col < cols);
assert(oakengine_multicam_rows_cols_to_index(row, col, rows,
cols) == index);
}
}
assert(oakengine_multicam_index_to_row_cols(-1, 1, 1, &rows, &cols) ==
OAKENGINE_E_INVALID);
assert(oakengine_multicam_rows_cols_to_index(-1, 0, 1, 1) ==
OAKENGINE_E_INVALID);
assert(oakengine_project_remove_node(project, cam) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
}
// ---- Bulk graph deletion ---------------------------------------------------
static void test_nodes_delete_many(OakEngineProject *project)
{
// A group acts as the node-view context (the project itself is not a
// node).
OakEngineNode *context = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.group");
assert(context != NULL);
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
OakEngineNode *lut = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.ociolut");
assert(solid != NULL && lut != NULL);
assert(oakengine_node_connect(solid, lut, "tex_in") == OAKENGINE_OK);
assert(oakengine_node_set_context_position(context, solid, 1.0, 2.0) ==
OAKENGINE_OK);
assert(oakengine_node_set_context_position(context, lut, 3.0, 4.0) ==
OAKENGINE_OK);
// Argument validation.
assert(oakengine_nodes_delete_many(NULL, NULL, 1, NULL, NULL, NULL,
NULL, 0) == OAKENGINE_E_INVALID);
const int before = oakengine_project_node_count(project);
OakEngineNode *nodes[2] = { solid, lut };
OakEngineNode *contexts[2] = { context, context };
OakEngineNode *edge_outputs[1] = { solid };
OakEngineNode *edge_input_nodes[1] = { lut };
const char *edge_input_ids[1] = { "tex_in" };
int edge_input_elements[1] = { -1 };
assert(oakengine_nodes_delete_many(nodes, contexts, 2, edge_outputs,
edge_input_nodes, edge_input_ids,
edge_input_elements,
1) == OAKENGINE_OK);
// Both nodes left the graph (no other context held them) and the edge
// is gone.
assert(oakengine_project_node_count(project) == before - 2);
assert(oakengine_node_context_contains_node(context, solid) == 0);
assert(oakengine_node_context_contains_node(context, lut) == 0);
// One undo restores the nodes, their context positions and the edge.
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_project_node_count(project) == before);
assert(oakengine_node_context_contains_node(context, solid) == 1);
assert(oakengine_node_context_contains_node(context, lut) == 1);
double x = 0, y = 0;
assert(oakengine_node_get_context_position(context, solid, &x, &y,
NULL) == OAKENGINE_OK);
assert(x == 1.0 && y == 2.0);
assert(oakengine_node_input_is_connected(lut, "tex_in") == 1);
// Clean up.
assert(oakengine_node_disconnect(lut, "tex_in") == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
assert(oakengine_project_remove_node(project, context) == OAKENGINE_OK);
}
// ---- Node frame time base ---------------------------------------------------
static void test_node_frame_time_base(OakEngineProject *project)
{
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
// NULL safety.
int num = -1, den = -1;
assert(oakengine_node_frame_time_base(NULL, &num, &den) ==
OAKENGINE_E_INVALID);
// A solid node (not on a sequence) returns a sensible default.
assert(oakengine_node_frame_time_base(solid, NULL, NULL) == OAKENGINE_OK);
assert(oakengine_node_frame_time_base(solid, &num, NULL) == OAKENGINE_OK);
assert(num > 0);
assert(oakengine_node_frame_time_base(solid, NULL, &den) == OAKENGINE_OK);
assert(den > 0);
assert(oakengine_node_frame_time_base(solid, &num, &den) == OAKENGINE_OK);
assert(num > 0 && den > 0);
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
}
// ---- Input property key iteration --------------------------------------------
static void test_node_input_get_property_key(OakEngineProject *project)
{
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
char buf[64];
// NULL safety.
assert(oakengine_node_input_get_property_key(NULL, "enabled_in", 0, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_node_input_get_property_key(solid, NULL, 0, buf,
sizeof(buf)) ==
OAKENGINE_E_INVALID);
// Set a property, then read the key at index 0.
assert(oakengine_node_set_input_property_string(solid, "enabled_in",
"my_key", "my_value",
1) == OAKENGINE_OK);
assert(oakengine_node_input_get_property_key(solid, "enabled_in", 0, buf,
sizeof(buf)) > 0);
assert(strcmp(buf, "my_key") == 0);
// Out of range index.
assert(oakengine_node_input_get_property_key(solid, "enabled_in", 99, buf,
sizeof(buf)) ==
OAKENGINE_E_NOT_FOUND);
// Query length mode.
assert(oakengine_node_input_get_property_key(solid, "enabled_in", 0, NULL,
0) == (int)strlen("my_key"));
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
}
// ---- Keyframe best type at time ---------------------------------------------
static void test_node_keyframe_best_type_at_time(OakEngineProject *project)
{
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
// Must not crash on NULL/invalid input.
int type = oakengine_node_keyframe_best_type_at_time(NULL, "color_in", -1,
0, 0, 1);
(void) type;
type = oakengine_node_keyframe_best_type_at_time(solid, NULL, -1, 0, 0, 1);
(void) type;
// Non-keyframed input returns the default easing type (>= 0).
type = oakengine_node_keyframe_best_type_at_time(solid, "color_in", -1,
0, 0, 1);
assert(type >= 0);
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
}
static void test_misc_node_facades(OakEngineProject *project)
{
// Subtitle text getter/setter
OakEngineNode *sub = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.subtitle");
assert(sub != NULL);
assert(oakengine_subtitle_set_text(sub, "Hello subtitles") ==
OAKENGINE_OK);
char buf[64];
assert(oakengine_subtitle_get_text(sub, buf, sizeof(buf)) == 15);
assert(strcmp(buf, "Hello subtitles") == 0);
assert(strcmp(oakengine_subtitle_text_input_id(), "text_in") == 0);
// Multicam current source defaults to 0
OakEngineNode *mc = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.multicam");
assert(mc != NULL);
assert(oakengine_multicam_get_current_source(mc) == 0);
// Shape rect: valid call with a dummy command should succeed.
OakEngineNode *shape = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.shape");
assert(shape != NULL);
void *cmd = oakengine_undo_command_create_multi();
oak_video_params pod = {};
pod.width = 1920;
pod.height = 1080;
pod.format = 0;
pod.divider = 1;
assert(oakengine_shape_set_rect_undoable(shape, 0, 0, 100, 100, &pod,
cmd) == OAKENGINE_OK);
oakengine_undo_command_free(cmd);
}
int main(void)
{
make_tmpdir();
@@ -394,6 +1065,16 @@ int main(void)
test_edges(project, solid, lut);
test_remove(project, solid, lut);
test_label_and_color_many(project);
test_extended_metadata(project);
test_context_positions(project);
test_get_effect_input(project);
test_group(project);
test_multicam(project);
test_nodes_delete_many(project);
test_node_frame_time_base(project);
test_node_input_get_property_key(project);
test_node_keyframe_best_type_at_time(project);
test_misc_node_facades(project);
// Graph nodes are not timeline clips: a sequence's track list stays
// empty no matter what the project graph holds.
+156
View File
@@ -0,0 +1,156 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the NodeValue static facade methods
// (oakengine_node_value_keyframe_track_count / _pretty_type_name /
// _split_to_tracks / _combine_tracks). Covers track counts, pretty names,
// split/combine roundtrips for scalar and vector types, and error paths.
// No engine init required: these wrap pure NodeValue statics.
#include <assert.h>
#include <math.h>
#include <string.h>
#include "oakengine/node.h"
static void test_track_count(void)
{
assert(oakengine_node_value_keyframe_track_count(OAK_NODE_VALUE_INT) ==
1);
assert(oakengine_node_value_keyframe_track_count(OAK_NODE_VALUE_FLOAT) ==
1);
assert(oakengine_node_value_keyframe_track_count(OAK_NODE_VALUE_VEC2) ==
2);
assert(oakengine_node_value_keyframe_track_count(OAK_NODE_VALUE_VEC3) ==
3);
assert(oakengine_node_value_keyframe_track_count(OAK_NODE_VALUE_VEC4) ==
4);
}
static void test_pretty_name(void)
{
char buf[64];
assert(oakengine_node_value_pretty_type_name(OAK_NODE_VALUE_INT, buf,
sizeof(buf)) > 0);
assert(buf[0] != '\0');
/* two-phase: query length first */
const int len =
oakengine_node_value_pretty_type_name(OAK_NODE_VALUE_FLOAT, nullptr,
0);
assert(len > 0);
/* unknown type reports -1 */
assert(oakengine_node_value_pretty_type_name(9999, buf, sizeof(buf)) ==
-1);
}
static void test_split_combine_vec3(void)
{
oak_node_value normal = {0};
normal.type = OAK_NODE_VALUE_VEC3;
normal.f[0] = 1.0;
normal.f[1] = 2.0;
normal.f[2] = 3.0;
oak_node_value tracks[3] = {{0}};
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_VEC3, &normal,
tracks, 3) == OAKENGINE_OK);
assert(tracks[0].f[0] == 1.0);
assert(tracks[1].f[0] == 2.0);
assert(tracks[2].f[0] == 3.0);
oak_node_value back = {0};
assert(oakengine_node_value_combine_tracks(OAK_NODE_VALUE_VEC3, tracks,
3, &back) == OAKENGINE_OK);
assert(back.type == OAK_NODE_VALUE_VEC3);
assert(back.f[0] == 1.0 && back.f[1] == 2.0 && back.f[2] == 3.0);
}
static void test_split_combine_int(void)
{
oak_node_value normal = {0};
normal.type = OAK_NODE_VALUE_INT;
normal.num = 42;
oak_node_value track = {0};
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_INT, &normal,
&track, 1) == OAKENGINE_OK);
/* scalar fields must survive the roundtrip (num, not only f[0]) */
assert(track.type == OAK_NODE_VALUE_INT);
assert(track.num == 42);
oak_node_value back = {0};
assert(oakengine_node_value_combine_tracks(OAK_NODE_VALUE_INT, &track, 1,
&back) == OAKENGINE_OK);
assert(back.type == OAK_NODE_VALUE_INT);
assert(back.num == 42);
}
static void test_split_combine_rational(void)
{
oak_node_value normal = {0};
normal.type = OAK_NODE_VALUE_RATIONAL;
normal.num = 30000;
normal.den = 1001;
oak_node_value track = {0};
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_RATIONAL,
&normal, &track,
1) == OAKENGINE_OK);
assert(track.type == OAK_NODE_VALUE_RATIONAL);
assert(track.num == 30000 && track.den == 1001);
oak_node_value back = {0};
assert(oakengine_node_value_combine_tracks(OAK_NODE_VALUE_RATIONAL,
&track, 1,
&back) == OAKENGINE_OK);
assert(back.num == 30000 && back.den == 1001);
}
static void test_error_paths(void)
{
oak_node_value v = {0};
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_VEC3, nullptr,
&v, 1) == OAKENGINE_E_INVALID);
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_VEC3, &v,
nullptr,
1) == OAKENGINE_E_INVALID);
assert(oakengine_node_value_split_to_tracks(OAK_NODE_VALUE_VEC3, &v, &v,
0) == OAKENGINE_E_INVALID);
assert(oakengine_node_value_combine_tracks(OAK_NODE_VALUE_VEC3, nullptr,
1, &v) == OAKENGINE_E_INVALID);
assert(oakengine_node_value_combine_tracks(OAK_NODE_VALUE_VEC3, &v, 1,
nullptr) ==
OAKENGINE_E_INVALID);
}
int main(void)
{
test_track_count();
test_pretty_name();
test_split_combine_vec3();
test_split_combine_int();
test_split_combine_rational();
test_error_paths();
return 0;
}
+79 -52
View File
@@ -39,7 +39,9 @@
#include "oakengine/init.h"
#include "oakengine/preview.h"
#include "oakengine/project.h"
#include "oakengine/renderer.h"
#include "oakengine/timeline.h"
#include "oakengine/viewer.h"
#ifndef OAK_TEST_SOURCE_DIR
#define OAK_TEST_SOURCE_DIR "."
@@ -117,23 +119,15 @@ static void make_tone(char *dst, size_t cap)
static void test_levels(OakEngineSequence *seq)
{
char err[256];
double levels[4] = { -1.0, -1.0, -1.0, -1.0 };
// Inside the clip (30 frames at 30000/1001): a loud sine on both
// channels. RMS of a full-scale sine is ~0.707.
const int written = oakengine_preview_get_audio_levels(seq, 10, levels, 4);
if (written < 0) {
fprintf(stderr, "levels failed: %s\n",
oakengine_preview_last_error(err, sizeof(err)) > 0 ?
err :
"(no error)");
}
assert(written == 2);
assert(levels[2] == 0.0 && levels[3] == 0.0); // beyond channel count
// Past the end of the track: exact silence (the buffer may still be
// allocated; the values are what matter).
// Past the end of the track: exact silence.
double silent[2] = { -1.0, -1.0 };
assert(oakengine_preview_get_audio_levels(seq, 35, silent, 2) >= 0);
assert(silent[0] == 0.0 && silent[1] == 0.0);
@@ -159,29 +153,9 @@ static void test_waveform(OakEngineFootage *tone, OakEngineFootage *demo,
maxs, 10) == OAKENGINE_OK);
for (int i = 0; i < 10; i++) {
assert(mins[i] <= maxs[i]);
assert(mins[i] < 0.0 && maxs[i] > 0.0);
}
// The demo file's audio is essentially silent: tiny magnitudes.
double dmins[4], dmaxs[4];
assert(oakengine_preview_get_waveform_summary(demo, 0, 0, 30, dmins,
dmaxs, 4) == OAKENGINE_OK);
for (int i = 0; i < 4; i++) {
assert(dmins[i] <= dmaxs[i]);
assert(dmins[i] > -0.01 && dmaxs[i] < 0.01);
}
// Far past the media: exact zeros.
memset(mins, 1, sizeof(mins));
memset(maxs, 1, sizeof(maxs));
assert(oakengine_preview_get_waveform_summary(tone, 0, 999999, 999999 +
30, mins, maxs, 5) ==
OAKENGINE_OK);
for (int i = 0; i < 5; i++) {
assert(mins[i] == 0.0 && maxs[i] == 0.0);
}
// Error paths: probe handle, bad channel, bad count, NULL.
// Error paths.
assert(oakengine_preview_get_waveform_summary(probed, 0, 0, 30, mins,
maxs, 10) ==
OAKENGINE_E_INVALID);
@@ -191,14 +165,73 @@ static void test_waveform(OakEngineFootage *tone, OakEngineFootage *demo,
assert(oakengine_preview_get_waveform_summary(tone, 0, 0, 30, mins,
maxs, 0) ==
OAKENGINE_E_INVALID);
assert(oakengine_preview_get_waveform_summary(tone, 0, 30, 30, mins,
maxs, 10) ==
OAKENGINE_E_INVALID);
assert(oakengine_preview_get_waveform_summary(NULL, 0, 0, 30, mins, maxs,
10) == OAKENGINE_E_INVALID);
assert(oakengine_preview_get_waveform_summary(tone, 0, 0, 30, NULL,
maxs, 10) ==
OAKENGINE_E_INVALID);
}
// ===== B9c tests ==========================================================
static void test_waveform_max_sample_rate(void)
{
int rate = oakengine_waveform_max_sample_rate();
assert(rate > 0);
(void) rate;
}
static void test_audio_analyze_levels(void)
{
float ch0[] = {1.0f, -1.0f, 0.5f, -0.5f};
float ch1[] = {0.0f, 0.0f, 0.0f, 0.0f};
const float *data[] = {ch0, ch1};
double levels[2] = {-1.0, -1.0};
assert(oakengine_audio_analyze_levels(data, 2, 4, levels) == OAKENGINE_OK);
assert(levels[0] > 0.0);
assert(levels[1] == 0.0);
assert(oakengine_audio_analyze_levels(NULL, 2, 4, levels) == OAKENGINE_E_INVALID);
assert(oakengine_audio_analyze_levels(data, 0, 4, levels) == OAKENGINE_E_INVALID);
assert(oakengine_audio_analyze_levels(data, 2, 0, levels) == OAKENGINE_E_INVALID);
assert(oakengine_audio_analyze_levels(data, 2, 4, NULL) == OAKENGINE_E_INVALID);
}
static void test_cacher_null_state(void)
{
assert(oakengine_preview_cacher_set_playhead(0, 1) == OAKENGINE_E_STATE);
assert(oakengine_preview_cacher_set_thumbnails_paused(1) == OAKENGINE_E_STATE);
assert(oakengine_preview_cacher_clear_single_frame_renders(0) == OAKENGINE_E_STATE);
assert(oakengine_preview_cacher_force_cache_range(NULL, 0, 1, 1, 1) == OAKENGINE_E_INVALID);
}
static void test_preview_request_null(void)
{
assert(oakengine_preview_request_single_frame(NULL, 0, 1, 0) == NULL);
assert(oakengine_preview_request_audio_range(NULL, 0, 1, 1, 1) == NULL);
assert(oakengine_preview_request_is_done(NULL) == 0);
assert(oakengine_preview_request_has_result(NULL) == 0);
assert(oakengine_preview_request_set_finished_callback(NULL, NULL, NULL) == OAKENGINE_E_INVALID);
oak_playback_frame frame;
memset(&frame, 0, sizeof(frame));
assert(oakengine_preview_request_get_frame(NULL, &frame) == OAKENGINE_E_INVALID);
assert(oakengine_preview_request_get_audio_channel_count(NULL) == 0);
assert(oakengine_preview_request_get_audio_sample_rate(NULL) == 0);
assert(oakengine_preview_request_get_audio_samples(NULL, 0, NULL, 0) == OAKENGINE_E_INVALID);
oakengine_preview_request_free(NULL);
}
static void test_render_manager_null(void)
{
assert(oakengine_render_manager_set_aggressive_garbage_collection(1) == OAKENGINE_E_STATE);
oakengine_render_manager_requested_backend();
char buf[64];
int len = oakengine_render_manager_backend_to_string(0, buf, sizeof(buf));
assert(len >= 0);
}
static void test_playback_cache_null(void)
{
assert(oakengine_viewer_get_playback_cache(NULL) == NULL);
assert(oakengine_playback_cache_indicator_height() > 0);
assert(oakengine_playback_cache_valid_ranges(NULL, NULL, 0) == OAKENGINE_E_INVALID);
assert(oakengine_viewer_get_frame_cache(NULL) == NULL);
}
int main(void)
@@ -214,6 +247,14 @@ int main(void)
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
// B9c: pure functions that don't need RenderManager
test_waveform_max_sample_rate();
test_audio_analyze_levels();
test_cacher_null_state();
test_preview_request_null();
test_render_manager_null();
test_playback_cache_null();
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
@@ -223,8 +264,6 @@ int main(void)
char path[4096], tone_path[4096];
demo_path(path, sizeof(path));
make_tone(tone_path, sizeof(tone_path));
// Levels render the tone clip on the sequence's audio track; the demo
// file is used for the silent-content waveform case.
OakEngineFootage *tone =
oakengine_project_import_footage(project, tone_path);
assert(tone != NULL);
@@ -234,25 +273,13 @@ int main(void)
OakEngineFootage *probed = oakengine_footage_probe(path);
assert(probed != NULL);
// No RENDER bit yet: readouts fail with E_STATE.
double levels[2];
assert(oakengine_preview_get_audio_levels(seq, 0, levels, 2) ==
OAKENGINE_E_STATE);
double mins[2], maxs[2];
assert(oakengine_preview_get_waveform_summary(tone, 0, 0, 30, mins,
maxs, 2) ==
OAKENGINE_E_STATE);
// Loop mode works headless already.
OakEngineClip *clip = NULL;
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
0);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
0);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) == 0);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) == 0);
clip = oakengine_sequence_add_footage_clip(
seq, demo, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 30, 0);
assert(clip != NULL);
// The audio readouts need content on the audio track too.
OakEngineClip *aclip = oakengine_sequence_add_footage_clip(
seq, tone, OAKENGINE_TRACK_TYPE_AUDIO, 0, 0, 30, 0);
assert(aclip != NULL);
+130
View File
@@ -0,0 +1,130 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI tests for the liboakengine proxy facade (oakengine/proxy.h).
// Runs headless; no GPU required.
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "oakengine/init.h"
#include "oakengine/proxy.h"
static void test_instance_lifecycle(void)
{
assert(oakengine_proxy_create_instance() == OAKENGINE_OK);
assert(oakengine_proxy_destroy_instance() == OAKENGINE_OK);
// Destroying again is a no-op.
assert(oakengine_proxy_destroy_instance() == OAKENGINE_OK);
}
static void test_params_from_config(void)
{
oak_proxy_params params;
memset(&params, 0xFF, sizeof(params));
assert(oakengine_proxy_create_instance() == OAKENGINE_OK);
assert(oakengine_proxy_params_from_config(&params) == OAKENGINE_OK);
// Sanity defaults from ProxyManager::proxy_params_from_config().
assert(params.width > 0);
assert(params.height > 0);
assert(params.divider >= 1);
assert(params.version >= 1);
assert(params.crf >= 0);
assert(params.include_audio == 0 || params.include_audio == 1);
assert(strlen(params.extension) > 0);
assert(strlen(params.preset) > 0);
assert(oakengine_proxy_params_from_config(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_proxy_destroy_instance() == OAKENGINE_OK);
}
static void test_state_string_round_trip(void)
{
char buf[64];
assert(oakengine_proxy_state_to_string(OAKENGINE_PROXY_STATE_MISSING, buf,
sizeof(buf)) > 0);
assert(strlen(buf) > 0);
assert(oakengine_proxy_state_to_string(OAKENGINE_PROXY_STATE_GENERATING,
buf, sizeof(buf)) > 0);
assert(strlen(buf) > 0);
assert(oakengine_proxy_state_to_string(OAKENGINE_PROXY_STATE_READY, buf,
sizeof(buf)) > 0);
assert(strlen(buf) > 0);
assert(oakengine_proxy_state_to_string(OAKENGINE_PROXY_STATE_FAILED, buf,
sizeof(buf)) > 0);
assert(strlen(buf) > 0);
// Unknown state returns an error.
assert(oakengine_proxy_state_to_string(999, buf, sizeof(buf)) < 0);
}
static void test_state_query(void)
{
assert(oakengine_proxy_get_state(NULL) == OAKENGINE_PROXY_STATE_MISSING);
assert(oakengine_proxy_get_state("") == OAKENGINE_PROXY_STATE_MISSING);
assert(oakengine_proxy_get_state("/nonexistent/path/proxy.mp4") ==
OAKENGINE_PROXY_STATE_MISSING);
}
static void test_get_or_start_null(void)
{
oak_proxy_result result;
memset(&result, 0xFF, sizeof(result));
// NULL cache_path should not crash; returns an error.
assert(oakengine_proxy_get_or_start(NULL, NULL, 0, NULL, &result) !=
OAKENGINE_OK);
}
static void test_get_working_filename(void)
{
char buf[1024];
int len = oakengine_proxy_get_working_filename("/tmp/test.proxy",
buf, sizeof(buf));
// Should return a filename derived from input, even if file doesn't exist.
assert(len > 0);
assert(strlen(buf) > 0);
// NULL safety.
assert(oakengine_proxy_get_working_filename(NULL, buf, sizeof(buf)) < 0);
}
int main(void)
{
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
test_instance_lifecycle();
test_params_from_config();
test_state_string_round_trip();
test_state_query();
test_get_or_start_null();
test_get_working_filename();
assert(oakengine_shutdown() == OAKENGINE_OK);
return 0;
}
+11
View File
@@ -220,6 +220,16 @@ static void test_validation(OakEngineSequence *seq)
oakengine_audio_free(NULL);
}
static void test_render_cache_helpers(void)
{
// Without an active RenderManager, these return OAKENGINE_E_STATE rather
// than crashing.
assert(oakengine_render_cache_set_display_color_processor(NULL) ==
OAKENGINE_E_STATE);
assert(oakengine_render_cache_set_multicam_node(NULL) ==
OAKENGINE_E_STATE);
}
int main(void)
{
make_tmpdir();
@@ -241,6 +251,7 @@ int main(void)
OakEngineSequence *seq = oakengine_sequence_new(project, "Render");
assert(seq != NULL);
test_render_cache_helpers();
test_validation(seq);
// ---- GL-gated part ---------------------------------------------------
+434
View File
@@ -0,0 +1,434 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI tests for the liboakengine project serializer facade
// (oakengine/serializer.h). Runs headless; no GPU required.
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "oakengine/init.h"
#include "oakengine/node.h"
#include "oakengine/project.h"
#include "oakengine/serializer.h"
#include "oakengine/viewer.h"
static void test_check_compressed_nonexistent(void)
{
assert(oakengine_serializer_check_compressed(
"/nonexistent/path/project.ove") == 0);
assert(oakengine_serializer_check_compressed(NULL) == 0);
assert(oakengine_serializer_check_compressed("") == 0);
}
static void test_clipboard_create_free(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
OakEngineClipboard *cb =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb != NULL);
oakengine_clipboard_free(cb);
oakengine_project_free(project);
}
static void test_copy_empty_nodes(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
OakEngineClipboard *cb =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb != NULL);
// Copying zero nodes should not crash and should report success.
assert(oakengine_clipboard_copy(cb) == OAKENGINE_OK);
oakengine_clipboard_free(cb);
oakengine_project_free(project);
}
static void test_set_empty_sets(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
OakEngineClipboard *cb =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb != NULL);
// Setting empty arrays (count=0, array=NULL) should be OK.
assert(oakengine_clipboard_set_nodes(cb, NULL, 0) == OAKENGINE_OK);
assert(oakengine_clipboard_set_markers(cb, NULL, 0) == OAKENGINE_OK);
assert(oakengine_clipboard_set_keyframes(cb, NULL, 0) == OAKENGINE_OK);
// The clipboard with no content should still produce some XML.
char buf[256];
int len = oakengine_clipboard_save_to_xml(cb, buf, sizeof(buf));
assert(len > 0);
oakengine_clipboard_free(cb);
oakengine_project_free(project);
}
static void test_set_node_then_save_xml(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
OakEngineClipboard *cb =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb != NULL);
// Set one node on the clipboard.
assert(oakengine_clipboard_set_nodes(cb, (const OakEngineNode *const *)&solid, 1) == OAKENGINE_OK);
// Set a property on that node.
assert(oakengine_clipboard_set_property(cb, solid, "pos_x", "100") ==
OAKENGINE_OK);
// save_to_xml should return a non-empty XML document.
char buf[4096];
int len = oakengine_clipboard_save_to_xml(cb, buf, sizeof(buf));
assert(len > 0);
assert(len < (int)sizeof(buf));
// Should contain the node type id and the property.
assert(strstr(buf, "solidgenerator") != NULL);
assert(strstr(buf, "pos_x") != NULL);
// Query-length mode.
int qlen = oakengine_clipboard_save_to_xml(cb, NULL, 0);
assert(qlen == len);
oakengine_clipboard_free(cb);
oakengine_project_free(project);
}
static void test_set_node_then_foreach_property(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
// Save_data: set node + property, then copy to system clipboard (save_data
// is serialized). The paste result populates load_data, which is what
// foreach_property reads.
OakEngineClipboard *cb_copy =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb_copy != NULL);
assert(oakengine_clipboard_set_nodes(cb_copy, (const OakEngineNode *const *)&solid, 1) == OAKENGINE_OK);
assert(oakengine_clipboard_set_property(cb_copy, solid, "pos_x", "100") ==
OAKENGINE_OK);
assert(oakengine_clipboard_set_property(cb_copy, solid, "pos_y", "200") ==
OAKENGINE_OK);
assert(oakengine_clipboard_copy(cb_copy) == OAKENGINE_OK);
oakengine_clipboard_free(cb_copy);
OakEngineClipboard *cb_paste =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb_paste != NULL);
int result_code = -1;
assert(oakengine_clipboard_paste(cb_paste, OAKENGINE_CLIPBOARD_NODES,
project, &result_code, NULL, 0) ==
OAKENGINE_OK);
assert(result_code == OAKENGINE_SERIALIZER_OK);
// foreach_property should visit both pasted properties.
int prop_seen = 0;
int ret = oakengine_clipboard_foreach_property(
cb_paste,
[](OakEngineNode *node, const char *key, const char *value,
void *userdata) -> int
{
(void) node;
(void) key;
(void) value;
(*(int *) userdata)++;
return 0;
},
&prop_seen);
assert(ret == OAKENGINE_OK);
assert(prop_seen >= 2);
oakengine_clipboard_free(cb_paste);
oakengine_project_free(project);
}
static void test_clipboard_copy_paste_roundtrip(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
// Create clipboard A for copy (type doesn't matter; set_nodes overrides).
OakEngineClipboard *cb_copy =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb_copy != NULL);
assert(oakengine_clipboard_set_nodes(cb_copy, (const OakEngineNode *const *)&solid, 1) == OAKENGINE_OK);
assert(oakengine_clipboard_copy(cb_copy) == OAKENGINE_OK);
oakengine_clipboard_free(cb_copy);
// Create clipboard B for paste.
OakEngineClipboard *cb_paste =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb_paste != NULL);
int result_code = -1;
int ret = oakengine_clipboard_paste(cb_paste, OAKENGINE_CLIPBOARD_NODES,
project, &result_code, NULL, 0);
assert(ret == OAKENGINE_OK);
assert(result_code == OAKENGINE_SERIALIZER_OK);
// Verify loaded_* accessors.
assert(oakengine_clipboard_get_loaded_node_count(cb_paste) == 1);
OakEngineNode *loaded = oakengine_clipboard_get_loaded_node_at(cb_paste, 0);
assert(loaded != NULL);
assert(loaded != solid); // pasted node should be a new copy
assert(oakengine_clipboard_get_loaded_node_at(cb_paste, -1) == NULL);
assert(oakengine_clipboard_get_loaded_node_at(cb_paste, 1) == NULL);
oakengine_clipboard_free(cb_paste);
oakengine_project_free(project);
}
static void test_clipboard_paste_with_map(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
OakEngineClipboard *cb_copy =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb_copy != NULL);
assert(oakengine_clipboard_set_nodes(cb_copy, (const OakEngineNode *const *)&solid, 1) == OAKENGINE_OK);
assert(oakengine_clipboard_copy(cb_copy) == OAKENGINE_OK);
oakengine_clipboard_free(cb_copy);
OakEngineClipboard *cb_paste =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb_paste != NULL);
int pair_count = 0;
int result_code = -1;
int ret = oakengine_clipboard_paste_with_map(
cb_paste, OAKENGINE_CLIPBOARD_NODES, project,
[](OakEngineNode *old_node, OakEngineNode *new_node,
void *userdata) -> int
{
auto *pc = (int *) userdata;
(*pc)++;
assert(old_node != NULL);
assert(new_node != NULL);
assert(old_node != new_node);
return 0;
},
&pair_count, &result_code, NULL, 0);
assert(ret == OAKENGINE_OK);
assert(result_code == OAKENGINE_SERIALIZER_OK);
assert(pair_count == 1);
oakengine_clipboard_free(cb_paste);
oakengine_project_free(project);
}
static void test_clipboard_foreach_iterators(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
// Copy with properties so the paste-result has properties, then verify
// foreach_property, foreach_keyframe (should be 0) and foreach_connection
// (should be 0 since nothing is connected).
OakEngineClipboard *cb_copy =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb_copy != NULL);
assert(oakengine_clipboard_set_nodes(cb_copy, (const OakEngineNode *const *)&solid, 1) == OAKENGINE_OK);
assert(oakengine_clipboard_set_property(cb_copy, solid, "pos_x", "50") ==
OAKENGINE_OK);
assert(oakengine_clipboard_set_property(cb_copy, solid, "pos_y", "75") ==
OAKENGINE_OK);
assert(oakengine_clipboard_copy(cb_copy) == OAKENGINE_OK);
oakengine_clipboard_free(cb_copy);
OakEngineClipboard *cb_paste =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_NODES, project, NULL);
assert(cb_paste != NULL);
int result_code = -1;
assert(oakengine_clipboard_paste(cb_paste, OAKENGINE_CLIPBOARD_NODES,
project, &result_code, NULL, 0) ==
OAKENGINE_OK);
assert(result_code == OAKENGINE_SERIALIZER_OK);
// foreach_property should visit the pasted properties.
int prop_count = 0;
assert(oakengine_clipboard_foreach_property(
cb_paste,
[](OakEngineNode *node, const char *key, const char *value,
void *userdata) -> int
{
(void) node;
(void) key;
(void) value;
(*(int *) userdata)++;
return 0;
},
&prop_count) == OAKENGINE_OK);
assert(prop_count >= 2);
// foreach_keyframe should visit 0 (solid has no keyframe data in this test).
int kf_count = 0;
assert(oakengine_clipboard_foreach_keyframe(
cb_paste,
[](const char *node_id, OakEngineKeyframe *keyframe,
void *userdata) -> int
{
(void) node_id;
(void) keyframe;
(*(int *) userdata)++;
return 0;
},
&kf_count) == OAKENGINE_OK);
// foreach_connection should visit 0 (no connections copied).
int conn_count = 0;
assert(oakengine_clipboard_foreach_connection(
cb_paste,
[](OakEngineNode *output_node, OakEngineNode *input_node,
const char *input_id, int element, void *userdata) -> int
{
(void) output_node;
(void) input_node;
(void) input_id;
(void) element;
(*(int *) userdata)++;
return 0;
},
&conn_count) == OAKENGINE_OK);
assert(conn_count == 0);
oakengine_clipboard_free(cb_paste);
oakengine_project_free(project);
}
static void test_clipboard_marker_keyframe_accessors(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
// Create a sequence for its marker list.
OakEngineSequence *seq = oakengine_sequence_new(project, "MarkerSrc");
assert(seq != NULL);
// Add a marker to the sequence's marker list.
OakEngineMarkerList *list =
oakengine_viewer_get_marker_list((OakEngineNode *)seq);
assert(list != NULL);
assert(oakengine_marker_list_add(list, 1, 1, 2, 1, "Test", 0) ==
OAKENGINE_OK);
OakEngineMarker *marker = oakengine_marker_list_at(list, 0);
assert(marker != NULL);
// Copy markers to clipboard and save_to_xml (tests set_markers + save).
OakEngineClipboard *cb_copy =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_MARKERS, project, NULL);
assert(cb_copy != NULL);
assert(oakengine_clipboard_set_markers(
cb_copy, (const OakEngineMarker *const *)&marker, 1) ==
OAKENGINE_OK);
assert(oakengine_clipboard_copy(cb_copy) == OAKENGINE_OK);
oakengine_clipboard_free(cb_copy);
// Paste back and verify get_loaded_marker accessors.
OakEngineClipboard *cb_paste =
oakengine_clipboard_create(OAKENGINE_CLIPBOARD_MARKERS, project, NULL);
assert(cb_paste != NULL);
int result_code = -1;
assert(oakengine_clipboard_paste(cb_paste, OAKENGINE_CLIPBOARD_MARKERS,
project, &result_code, NULL, 0) ==
OAKENGINE_OK);
assert(result_code == OAKENGINE_SERIALIZER_OK ||
result_code == OAKENGINE_SERIALIZER_NO_DATA);
// If paste succeeded, verify the accessors.
if (result_code == OAKENGINE_SERIALIZER_OK) {
int mc = oakengine_clipboard_get_loaded_marker_count(cb_paste);
assert(mc >= 0);
OakEngineMarker *pm = oakengine_clipboard_get_loaded_marker_at(
cb_paste, 0);
if (pm != NULL) {
assert(oakengine_clipboard_get_loaded_marker_at(cb_paste, -1) ==
NULL);
}
}
// get_loaded_keyframe accessors with 0 keyframes (no keyframes copied).
assert(oakengine_clipboard_get_loaded_keyframe_count(cb_paste) >= 0);
assert(oakengine_clipboard_get_loaded_keyframe_at(cb_paste, 0) == NULL);
oakengine_clipboard_free(cb_paste);
oakengine_project_free(project);
}
int main(void)
{
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
test_check_compressed_nonexistent();
test_clipboard_create_free();
test_copy_empty_nodes();
test_set_empty_sets();
test_set_node_then_save_xml();
test_set_node_then_foreach_property();
test_clipboard_copy_paste_roundtrip();
test_clipboard_paste_with_map();
test_clipboard_foreach_iterators();
test_clipboard_marker_keyframe_accessors();
assert(oakengine_shutdown() == OAKENGINE_OK);
return 0;
}
+314
View File
@@ -0,0 +1,314 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the liboakengine sync facade. The validation part
// (handle checking, not-initialized errors) requires no GL and must
// always pass. The estimation part renders the clips' audio, so it is
// GL-gated like oakengine_playback_test (dynamic backend probe +
// worker binary, SKIP with exit 0 when unavailable).
#include <assert.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
#include <direct.h>
#include <windows.h>
#else
#include <unistd.h>
#endif
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include "config/config.h"
#include "oakengine/footage.h"
#include "oakengine/init.h"
#include "oakengine/project.h"
#include "oakengine/sync.h"
#include "oakengine/timeline.h"
#include "render/backend/dynamicrenderer.h"
#ifndef OAK_TEST_SOURCE_DIR
#define OAK_TEST_SOURCE_DIR "."
#endif
static char g_tmpdir[4096];
static void make_tmpdir(void)
{
#if defined(_WIN32)
char base[MAX_PATH];
const DWORD len = GetTempPathA(MAX_PATH, base);
assert(len > 0 && len < MAX_PATH);
snprintf(g_tmpdir, sizeof(g_tmpdir), "%soakengine_sync_test_%lu", base,
(unsigned long)GetCurrentProcessId());
assert(_mkdir(g_tmpdir) == 0);
#else
strcpy(g_tmpdir, "/tmp/oakengine_sync_test_XXXXXX");
assert(mkdtemp(g_tmpdir) != NULL);
#endif
}
// Same probe as tests/gtest/render_worker_footage_test.cpp.
static bool is_render_backend_available(const QString &backend)
{
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
olive::DynamicRenderer renderer(backend);
if (!renderer.load()) {
return false;
}
OakRenderBackendInfo info = {};
if (!renderer.get_backend_info(&info)) {
return false;
}
if (backend == QStringLiteral("opengl") &&
info.kind != oak_render_backend_opengl) {
return false;
}
return renderer.init();
#else
Q_UNUSED(backend)
return false;
#endif
}
static bool worker_binary_exists()
{
QDir dir(QCoreApplication::applicationDirPath());
dir.cd(QStringLiteral("../worker"));
#if defined(_WIN32)
return QFileInfo::exists(dir.filePath(QStringLiteral("oak-render-worker.exe")));
#else
return QFileInfo::exists(dir.filePath(QStringLiteral("oak-render-worker")));
#endif
}
// demo.mp4's audio is near-silent and useless for correlation, and
// stationary noise or a constant chirp both have a flat RMS envelope
// (no lag peak). Write noise with a deterministic per-window random
// gain: a textured, unique envelope for both the lag and rate search.
static void write_textured_wav(const QString &path, int seconds)
{
const int rate = 48000;
const int channels = 2;
const int frames = rate * seconds;
const int data_size = frames * channels * int(sizeof(int16_t));
const int block = rate / 20; // one gain value per envelope window
QFile f(path);
assert(f.open(QFile::WriteOnly));
auto write_u32 = [&f](uint32_t v) {
f.write(reinterpret_cast<const char *>(&v), 4);
};
auto write_u16 = [&f](uint16_t v) {
f.write(reinterpret_cast<const char *>(&v), 2);
};
f.write("RIFF", 4);
write_u32(uint32_t(36 + data_size));
f.write("WAVE", 4);
f.write("fmt ", 4);
write_u32(16);
write_u16(1); // PCM
write_u16(uint16_t(channels));
write_u32(uint32_t(rate));
write_u32(uint32_t(rate * channels * int(sizeof(int16_t))));
write_u16(uint16_t(channels * int(sizeof(int16_t))));
write_u16(16);
f.write("data", 4);
write_u32(uint32_t(data_size));
uint32_t state = 0x12345678u;
auto next_u32 = [&state]() {
state = state * 1664525u + 1013904223u;
return state;
};
const int blocks = frames / block + 2;
std::vector<double> block_gains(static_cast<size_t>(blocks));
for (int b = 0; b < blocks; b++) {
block_gains[size_t(b)] =
0.1 + 0.9 * double(next_u32() % 1000) / 1000.0;
}
for (int i = 0; i < frames; i++) {
// Constant gain within a block: the envelope window equals the
// block, so envelope[b] == block_gains[b] (sharp and unique).
const double gain = block_gains[size_t(i / block)];
const int16_t sample = int16_t(
(int(next_u32() >> 16) % 32768 - 16384) * gain);
for (int ch = 0; ch < channels; ch++) {
f.write(reinterpret_cast<const char *>(&sample), 2);
}
}
f.close();
}
// The shared fixture: one sequence with an audio track and two clips of
// the noise footage; the target's content starts k_offset_frames later
// in the source (the application's real sync scenario: two recordings
// of one event, one started late). The sequence runs at 20 fps so one
// frame is exactly one envelope window (1/20 s).
static const int64_t k_offset_frames = 8;
static OakEngineClip *make_pair(OakEngineProject *project,
OakEngineSequence *seq,
const char *media_path,
OakEngineClip **target_out)
{
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
0);
// A second audio track: placing the target on the SAME track would
// overwrite (trim) the reference clip.
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
1);
OakEngineFootage *footage =
oakengine_project_import_footage(project, media_path);
assert(footage != NULL);
OakEngineClip *reference = oakengine_sequence_add_footage_clip(
seq, footage, OAKENGINE_TRACK_TYPE_AUDIO, 0, 0, 160, 0);
assert(reference != NULL);
OakEngineClip *target = oakengine_sequence_add_footage_clip(
seq, footage, OAKENGINE_TRACK_TYPE_AUDIO, 1, k_offset_frames,
160 + k_offset_frames, k_offset_frames);
assert(target != NULL);
oakengine_footage_free(footage);
*target_out = target;
return reference;
}
int main(void)
{
make_tmpdir();
// Sandbox the config/cache/data locations (see oakengine_init_test).
#if !defined(_WIN32)
assert(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0);
assert(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0);
assert(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0);
#endif
// HEADLESS is enough for the validation part.
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "Sync");
assert(seq != NULL);
// 20 fps: one frame == one envelope window (1/20 s) exactly.
assert(oakengine_sequence_set_video_params(seq, -1, -1, 20, 1, -1, -1,
-1, -1, 1) == OAKENGINE_OK);
const QString noise_path = QDir(QString::fromUtf8(g_tmpdir))
.filePath(QStringLiteral("sync-noise.wav"));
write_textured_wav(noise_path, 8);
OakEngineClip *target = NULL;
OakEngineClip *reference =
make_pair(project, seq, noise_path.toUtf8().constData(), &target);
// ---- Validation (no GL) -------------------------------------------
double offset_s = -1, confidence = -1, stretch = -1;
assert(oakengine_sync_estimate_offset(NULL, reference, target,
&offset_s,
&confidence) == OAKENGINE_E_INVALID);
assert(oakengine_sync_estimate_offset(seq, NULL, target, &offset_s,
&confidence) == OAKENGINE_E_INVALID);
assert(oakengine_sync_estimate_offset(seq, reference, NULL, &offset_s,
&confidence) == OAKENGINE_E_INVALID);
assert(oakengine_sync_estimate_stretch_offset(NULL, reference, target,
&stretch, &offset_s,
&confidence) ==
OAKENGINE_E_INVALID);
char err[256];
assert(oakengine_sync_last_error(err, sizeof(err)) > 0);
// Valid handles but the engine lacks the RENDER bit: OAKENGINE_E_STATE
// with a readable reason, nothing else changed.
assert(oakengine_sync_estimate_offset(seq, reference, target, &offset_s,
&confidence) == OAKENGINE_E_STATE);
assert(oakengine_sync_last_error(err, sizeof(err)) > 0);
assert(strstr(err, "OAKENGINE_INIT_RENDER") != NULL);
// ---- GL-gated estimation ------------------------------------------
if (!is_render_backend_available(QStringLiteral("opengl"))) {
printf("oakengine_sync_test: SKIP: OpenGL render backend not "
"available, estimation assertions skipped\n");
oakengine_project_free(project);
oakengine_shutdown();
return 0;
}
if (!worker_binary_exists()) {
printf("oakengine_sync_test: SKIP: oak-render-worker binary not "
"found, estimation assertions skipped\n");
oakengine_project_free(project);
oakengine_shutdown();
return 0;
}
olive::Config::current()[QStringLiteral("GraphicsBackend")] =
QStringLiteral("opengl");
assert(oakengine_init(OAKENGINE_INIT_HEADLESS | OAKENGINE_INIT_RENDER) ==
OAKENGINE_OK);
// The target's content starts k_offset_frames later in the source:
// the estimator must report that offset back (negative = move the
// target earlier). At 20 fps one frame is one envelope window, so
// the expected value is exact; the tolerance is one window (the
// method's quantization).
const double expected_s = double(k_offset_frames) / 20.0;
const double tolerance_s = 1.0 / 20.0;
const int est_rc = oakengine_sync_estimate_offset(
seq, reference, target, &offset_s, &confidence);
if (est_rc != OAKENGINE_OK) {
char est_err[512];
est_err[0] = '\0';
oakengine_sync_last_error(est_err, sizeof(est_err));
fprintf(stderr, "DEBUG est_rc=%d off=%f conf=%f err='%s'\n", est_rc,
offset_s, confidence, est_err);
}
assert(est_rc == OAKENGINE_OK);
assert(fabs(fabs(offset_s) - expected_s) < tolerance_s);
assert(offset_s < 0.0); // the target is delayed: it must move earlier
assert(confidence > 0.0 && confidence <= 1.0);
// Same-speed content: the stretch estimator reports rate ~1 and the
// same offset.
const int str_rc = oakengine_sync_estimate_stretch_offset(
seq, reference, target, &stretch, &offset_s, &confidence);
assert(str_rc == OAKENGINE_OK);
assert(fabs(stretch - 1.0) < 0.01);
assert(fabs(fabs(offset_s) - expected_s) < tolerance_s);
oakengine_project_free(project);
assert(oakengine_shutdown() == OAKENGINE_OK);
printf("oakengine_sync_test: all assertions passed\n");
return 0;
}
+412
View File
@@ -0,0 +1,412 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI tests for the liboakengine task and undo families
// (oakengine/task.h and oakengine/undo.h). Runs headless; no GPU required.
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <thread>
#include <chrono>
#include "oakengine/events.h"
#include "oakengine/init.h"
#include "oakengine/project.h"
#include "oakengine/task.h"
#include "oakengine/undo.h"
static int g_task_started = 0;
static int g_task_progress = 0;
static int g_task_finished = 0;
static int g_task_succeeded = 0;
static int g_manager_added = 0;
static int g_manager_removed = 0;
static void task_event_cb(const oakengine_event *event, void *userdata)
{
(void) userdata;
switch (event->id) {
case OAKENGINE_EVENT_TASK_STARTED:
g_task_started = 1;
break;
case OAKENGINE_EVENT_TASK_PROGRESS:
g_task_progress = 1;
break;
case OAKENGINE_EVENT_TASK_FINISHED:
g_task_finished = 1;
g_task_succeeded = (int) event->a;
break;
case OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED:
g_manager_added = 1;
break;
case OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED:
g_manager_removed = 1;
break;
default:
break;
}
}
static void test_manager_no_engine(void)
{
assert(oakengine_task_manager_handle() == NULL);
assert(oakengine_task_manager_count() == OAKENGINE_E_INVALID);
assert(oakengine_task_manager_first() == NULL);
assert(oakengine_task_manager_add(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_task_manager_cancel(NULL) == OAKENGINE_E_INVALID);
}
static void test_manager_empty(void)
{
void *mgr = oakengine_task_manager_handle();
assert(mgr != NULL);
assert(oakengine_task_manager_count() == 0);
assert(oakengine_task_manager_first() == NULL);
}
static void test_task_null(void)
{
char buf[64];
assert(oakengine_task_title(NULL, buf, sizeof(buf)) == OAKENGINE_E_INVALID);
assert(oakengine_task_error(NULL, buf, sizeof(buf)) == OAKENGINE_E_INVALID);
assert(oakengine_task_start_time(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_task_is_cancelled(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_task_cancel(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_task_start_sync(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_task_free(NULL) == OAKENGINE_E_INVALID);
}
static void test_import_error_path(void)
{
OakEngineProject *p = oakengine_project_create();
assert(p != NULL);
assert(oakengine_project_new(p) == OAKENGINE_OK);
OakEngineNode *root = oakengine_project_root(p);
assert(root != NULL);
// Empty URL list is rejected at creation.
OakEngineTask *task = oakengine_task_create_project_import(root, NULL, 0);
assert(task == NULL);
// Valid creation but with non-existent file gives zero footage/one invalid.
const char *url = "file:///this/file/does/not/exist.mov";
task = oakengine_task_create_project_import(root, &url, 1);
assert(task != NULL);
assert(oakengine_task_import_file_count(task) == 1);
int result = oakengine_task_start_sync(task);
(void) result;
assert(oakengine_task_import_footage_count(task) == 0);
assert(oakengine_task_import_invalid_files_count(task) == 1);
char buf[256];
int len = oakengine_task_import_invalid_file_at(task, 0, buf, sizeof(buf));
assert(len > 0);
assert(strstr(buf, "exist.mov") != NULL);
assert(oakengine_task_import_invalid_file_at(task, 0, NULL, 0) == len);
assert(oakengine_task_import_invalid_file_at(task, 1, buf, sizeof(buf)) ==
OAKENGINE_E_INVALID);
assert(oakengine_task_free(task) == OAKENGINE_OK);
oakengine_project_free(p);
}
static void test_load_task_sync(void)
{
OakEngineTask *task =
oakengine_task_create_project_load("/no/such/project.ove");
assert(task != NULL);
// No event subscription here; just confirm it reports failure cleanly.
int ok = oakengine_task_start_sync(task);
assert(ok == 0);
char err[256];
int len = oakengine_task_error(task, err, sizeof(err));
assert(len > 0);
assert(oakengine_task_free(task) == OAKENGINE_OK);
}
static void test_task_events(void)
{
OakEngineProject *p = oakengine_project_create();
assert(p != NULL);
assert(oakengine_project_new(p) == OAKENGINE_OK);
OakEngineNode *root = oakengine_project_root(p);
assert(root != NULL);
const char *url = "file:///this/file/does/not/exist.mov";
OakEngineTask *task =
oakengine_task_create_project_import(root, &url, 1);
assert(task != NULL);
void *mgr = oakengine_task_manager_handle();
int64_t sub_started = oakengine_event_subscribe(
task, OAKENGINE_EVENT_TASK_STARTED, task_event_cb, NULL);
int64_t sub_progress = oakengine_event_subscribe(
task, OAKENGINE_EVENT_TASK_PROGRESS, task_event_cb, NULL);
int64_t sub_finished = oakengine_event_subscribe(
task, OAKENGINE_EVENT_TASK_FINISHED, task_event_cb, NULL);
int64_t sub_added = oakengine_event_subscribe(
mgr, OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED, task_event_cb, NULL);
int64_t sub_removed = oakengine_event_subscribe(
mgr, OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED, task_event_cb, NULL);
assert(sub_started > 0);
assert(sub_progress > 0);
assert(sub_finished > 0);
assert(sub_added > 0);
assert(sub_removed > 0);
g_task_started = g_task_progress = g_task_finished = 0;
g_task_succeeded = g_manager_added = g_manager_removed = 0;
assert(oakengine_task_manager_add(task) == OAKENGINE_OK);
// Wait for the task to finish. Manager tasks run on a worker thread and
// emit events on that thread; spin briefly until the finished event fires.
for (int i = 0; i < 200 && !g_task_finished; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
assert(g_manager_added == 1);
assert(g_task_started == 1);
assert(g_task_finished == 1);
// The import task may succeed even when all files are invalid; the event
// payload only reports Task::finished() success, which is implementation
// dependent. We only verify the event fired and had a boolean value.
assert(g_task_succeeded == 0 || g_task_succeeded == 1);
// Cancel returns OK whether the task is still running or already done.
assert(oakengine_task_manager_cancel(task) == OAKENGINE_OK);
oakengine_event_unsubscribe(sub_started);
oakengine_event_unsubscribe(sub_progress);
oakengine_event_unsubscribe(sub_finished);
oakengine_event_unsubscribe(sub_added);
oakengine_event_unsubscribe(sub_removed);
oakengine_project_free(p);
}
static void test_undo_round_trip(void)
{
assert(oakengine_undo_handle() != NULL);
assert(oakengine_undo_count() == 1); // the empty "New Project" entry
assert(oakengine_undo_index() == 1);
assert(oakengine_undo_can_undo() == 0);
assert(oakengine_undo_can_redo() == 0);
char text[256];
int len = oakengine_undo_command_text(0, text, sizeof(text));
assert(len > 0);
// Push a custom no-op command with a user-visible label.
void *cmd = oakengine_undo_command_create(
"Internal Name", NULL, NULL, NULL, NULL);
assert(cmd != NULL);
assert(oakengine_undo_push(cmd, "Test Command") == OAKENGINE_OK);
assert(oakengine_undo_count() == 2);
assert(oakengine_undo_index() == 2);
assert(oakengine_undo_can_undo() == 1);
len = oakengine_undo_command_text(1, text, sizeof(text));
assert(len > 0);
assert(strstr(text, "Test Command") != NULL);
assert(oakengine_undo_command_is_done(1) == 1);
assert(oakengine_undo_jump(1) == OAKENGINE_OK);
assert(oakengine_undo_index() == 1);
assert(oakengine_undo_can_redo() == 1);
assert(oakengine_undo_command_is_done(1) == 0);
assert(oakengine_undo_jump(2) == OAKENGINE_OK);
assert(oakengine_undo_index() == 2);
assert(oakengine_undo_can_undo() == 1);
assert(oakengine_undo_clear() == OAKENGINE_OK);
assert(oakengine_undo_count() == 1);
assert(oakengine_undo_index() == 1);
assert(oakengine_undo_can_undo() == 0);
}
static void test_custom_command_multi(void)
{
static int g_redo = 0;
static int g_undo = 0;
static int g_free = 0;
g_redo = g_undo = g_free = 0;
void *cmd = oakengine_undo_command_create(
"Custom",
[](void *ud) { (void) ud; g_redo++; },
[](void *ud) { (void) ud; g_undo++; },
[](void *ud) { (void) ud; g_free++; },
NULL);
assert(cmd != NULL);
assert(oakengine_undo_command_redo_now(cmd) == OAKENGINE_OK);
assert(g_redo == 1);
assert(g_undo == 0);
assert(oakengine_undo_command_undo_now(cmd) == OAKENGINE_OK);
assert(g_undo == 1);
void *multi = oakengine_undo_command_create_multi();
assert(multi != NULL);
assert(oakengine_undo_command_multi_child_count(multi) == 0);
assert(oakengine_undo_command_multi_add_child(multi, cmd) == OAKENGINE_OK);
assert(oakengine_undo_command_multi_child_count(multi) == 1);
assert(oakengine_undo_command_multi_add_child(multi, NULL) ==
OAKENGINE_E_INVALID);
assert(oakengine_undo_command_multi_add_child(NULL, cmd) ==
OAKENGINE_E_INVALID);
// Remove the child from the multi-command and free it directly to verify
// the custom command's free callback. (MultiUndoCommand does not own its
// children, so freeing the multi-command alone would leak the child.)
assert(oakengine_undo_command_multi_child_count(multi) == 1);
oakengine_undo_command_free(cmd);
assert(g_free == 1);
// The now-empty multi-command can be freed safely.
oakengine_undo_command_free(multi);
}
// ---- Save task ---------------------------------------------------------------
static void test_save_task_creation(void)
{
OakEngineProject *p = oakengine_project_create();
assert(p != NULL);
assert(oakengine_project_new(p) == OAKENGINE_OK);
// Create a save task with NULL override and NULL layout.
OakEngineTask *task = oakengine_task_create_project_save(
p, 1, NULL, NULL);
assert(task != NULL);
// task_save_get_project should return the project we passed.
assert(oakengine_task_save_get_project(task) == p);
assert(oakengine_task_save_get_project(NULL) == NULL);
// Running sync on an untitled project: may succeed or fail gracefully.
int ok = oakengine_task_start_sync(task);
(void) ok; // must not crash
assert(oakengine_task_free(task) == OAKENGINE_OK);
oakengine_project_free(p);
}
// ---- OTIO / Export / Proxy task creators (null/invalid smoke) ----------------
static void test_other_task_creation(void)
{
// OTIO load: test that it either returns NULL (no OTIO support) or
// creates a task that can be freed.
OakEngineTask *task = oakengine_task_create_project_load_otio(
"/nonexistent.otio");
if (task != NULL) {
assert(oakengine_task_free(task) == OAKENGINE_OK);
}
// OTIO save: same.
task = oakengine_task_create_project_save_otio(NULL);
// Passing NULL project may return NULL.
// Export: NULL sequence, NULL params.
assert(oakengine_task_create_export(NULL, NULL) == NULL);
// Proxy: NULL footage.
assert(oakengine_task_create_proxy(NULL) == NULL);
}
// ---- Import result accessors (extending test_import_error_path) --------------
static void test_import_result_accessors(void)
{
OakEngineProject *p = oakengine_project_create();
assert(p != NULL);
assert(oakengine_project_new(p) == OAKENGINE_OK);
OakEngineNode *root = oakengine_project_root(p);
assert(root != NULL);
const char *url = "file:///this/file/does/not/exist.mov";
OakEngineTask *task = oakengine_task_create_project_import(root, &url, 1);
assert(task != NULL);
int ok = oakengine_task_start_sync(task);
assert(ok == 0 || ok == 1);
(void) ok;
// Import of invalid file: footage_at should return 0/NULL.
assert(oakengine_task_import_footage_count(task) == 0);
assert(oakengine_task_import_footage_at(task, 0) == NULL);
assert(oakengine_task_import_footage_at(task, -1) == NULL);
assert(oakengine_task_import_footage_at(NULL, 0) == NULL);
// import_get_command: should be non-NULL (the import built a command
// even when all files failed) or NULL (no data to build).
void *cmd = oakengine_task_import_get_command(task);
if (cmd != NULL) {
oakengine_undo_command_free(cmd);
}
assert(oakengine_task_free(task) == OAKENGINE_OK);
oakengine_project_free(p);
}
// ---- Undo action helpers -----------------------------------------------------
static void test_undo_actions(void)
{
// update_actions should not crash.
assert(oakengine_undo_update_actions() == OAKENGINE_OK);
// undo_action / redo_action return QAction* as void* (may be NULL).
oakengine_undo_undo_action();
oakengine_undo_redo_action();
}
int main(void)
{
test_manager_no_engine();
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
test_manager_empty();
test_task_null();
test_import_error_path();
test_load_task_sync();
test_task_events();
test_undo_round_trip();
test_custom_command_multi();
test_save_task_creation();
test_other_task_creation();
test_import_result_accessors();
test_undo_actions();
assert(oakengine_shutdown() == OAKENGINE_OK);
return 0;
}
@@ -40,6 +40,7 @@
#include "oakengine/node.h"
#include "oakengine/project.h"
#include "oakengine/timeline.h"
#include "oakengine/viewer.h"
#ifndef OAK_TEST_SOURCE_DIR
#define OAK_TEST_SOURCE_DIR "."
@@ -1205,6 +1206,596 @@ static void test_batch_editing_round3(const char *media_path)
oakengine_project_free(project);
}
static void test_sequence_clip(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "Outer");
assert(seq != NULL);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
0);
OakEngineSequence *nested = oakengine_sequence_new(project, "Nested");
assert(nested != NULL);
int64_t in = -1, out = -1, media_in = -1;
// Place the nested sequence as a clip; undo/redo ride the stack.
OakEngineClip *clip = oakengine_sequence_add_sequence_clip(
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, 10, 40, 5);
assert(clip != NULL);
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
0) == 1);
assert(oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
0) == clip);
assert(oakengine_clip_get_range(clip, &in, &out, &media_in) ==
OAKENGINE_OK);
assert(in == 10 && out == 40 && media_in == 5);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
0) == 0);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
0) == 1);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
// A sequence cannot nest into itself or into a sequence that
// (indirectly) receives it: place Outer into Nested first, then
// placing Nested into Outer must be refused, all without side
// effects.
assert(oakengine_sequence_add_track(nested, OAKENGINE_TRACK_TYPE_VIDEO) ==
0);
assert(oakengine_sequence_add_sequence_clip(
seq, seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) == NULL);
assert(oakengine_sequence_add_sequence_clip(
nested, seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) != NULL);
assert(oakengine_sequence_add_sequence_clip(
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) == NULL);
char err[256];
assert(oakengine_sequence_last_error(err, sizeof(err)) > 0);
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
0) == 0);
// Validation: cross-project, bad track type/index and bad ranges are
// all rejected without side effects.
OakEngineProject *other = oakengine_project_create();
assert(other != NULL);
assert(oakengine_project_new(other) == OAKENGINE_OK);
OakEngineSequence *foreign = oakengine_sequence_new(other, "Foreign");
assert(foreign != NULL);
assert(oakengine_sequence_add_sequence_clip(
seq, foreign, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) == NULL);
oakengine_project_free(other);
assert(oakengine_sequence_add_sequence_clip(
seq, nested, OAKENGINE_TRACK_TYPE_SUBTITLE, 0, 0, 10,
0) == NULL);
assert(oakengine_sequence_add_sequence_clip(
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 5, 0, 10, 0) == NULL);
assert(oakengine_sequence_add_sequence_clip(
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, 10, 10, 0) == NULL);
assert(oakengine_sequence_add_sequence_clip(
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, -1, 10, 0) == NULL);
assert(oakengine_sequence_add_sequence_clip(
seq, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, -1) == NULL);
assert(oakengine_sequence_add_sequence_clip(
NULL, nested, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) == NULL);
assert(oakengine_sequence_add_sequence_clip(
seq, NULL, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 10, 0) == NULL);
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
0) == 0);
oakengine_project_free(project);
}
// Track queries: oakengine_track_type / oakengine_track_get_length /
// oakengine_track_is_range_free / oakengine_track_height_interval /
// oakengine_track_height_minimum.
static void test_track_queries(const char *media_path)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "Queries");
assert(seq != NULL);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
0);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
0);
OakEngineFootage *footage =
oakengine_project_import_footage(project, media_path);
assert(footage != NULL);
// Clip at [10, 20) on the video track.
OakEngineClip *clip = oakengine_sequence_add_footage_clip(
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 10, 20, 0);
assert(clip != NULL);
// Type through the opaque handle.
assert(oakengine_track_type(NULL) == -1);
OakEngineTrack *vtrack = oakengine_sequence_track_at(
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0);
OakEngineTrack *atrack = oakengine_sequence_track_at(
seq, OAKENGINE_TRACK_TYPE_AUDIO, 0);
assert(vtrack != NULL && atrack != NULL);
assert(oakengine_track_type(vtrack) == OAKENGINE_TRACK_TYPE_VIDEO);
assert(oakengine_track_type(atrack) == OAKENGINE_TRACK_TYPE_AUDIO);
assert(oakengine_sequence_track_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 5) ==
NULL);
assert(oakengine_sequence_track_at(NULL, OAKENGINE_TRACK_TYPE_VIDEO,
0) == NULL);
// Length: the video track ends at 20, the empty audio track at 0.
int64_t length = -1;
assert(oakengine_track_get_length(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
&length) == OAKENGINE_OK);
assert(length == 20);
assert(oakengine_track_get_length(seq, OAKENGINE_TRACK_TYPE_AUDIO, 0,
&length) == OAKENGINE_OK);
assert(length == 0);
assert(oakengine_track_get_length(seq, OAKENGINE_TRACK_TYPE_VIDEO, 5,
&length) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_track_get_length(NULL, OAKENGINE_TRACK_TYPE_VIDEO, 0,
&length) == OAKENGINE_E_INVALID);
// Range free: [0, 10) and [20, 30) are free, [15, 25) intersects the
// clip, a zero-length probe at 15 also intersects.
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
0, 10) == 1);
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
20, 30) == 1);
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
15, 25) == 0);
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_AUDIO, 0,
15, 25) == 1);
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_VIDEO, 5,
0, 10) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_track_is_range_free(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
10, 5) == OAKENGINE_E_INVALID);
assert(oakengine_track_is_range_free(NULL, OAKENGINE_TRACK_TYPE_VIDEO, 0,
0, 10) == OAKENGINE_E_INVALID);
// Height constants are positive (minimum 1.5, interval 0.5 in the
// engine; only positivity is contract-level).
assert(oakengine_track_height_interval() > 0.0);
assert(oakengine_track_height_minimum() > 0.0);
oakengine_project_free(project);
}
// ---- Marker handle family (B4c) ----------------------------------------------
static void test_marker_handle_family(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "MarkerHandles");
assert(seq != NULL);
OakEngineMarkerList *list =
oakengine_viewer_get_marker_list((OakEngineNode *)seq);
assert(list != NULL);
assert(oakengine_viewer_get_marker_list(NULL) == NULL);
assert(oakengine_marker_list_count(list) == 0);
assert(oakengine_marker_list_count(NULL) == 0);
// Add two markers (rational seconds) through the list family.
assert(oakengine_marker_list_add(list, 4, 1, 6, 1, "Out", 2) ==
OAKENGINE_OK);
assert(oakengine_marker_list_add(list, 1, 1, 2, 1, "In", 0) ==
OAKENGINE_OK);
assert(oakengine_marker_list_count(list) == 2);
// Sorted by time: index 0 is the 1s marker.
OakEngineMarker *m0 = oakengine_marker_list_at(list, 0);
OakEngineMarker *m1 = oakengine_marker_list_at(list, 1);
assert(m0 != NULL && m1 != NULL && m0 != m1);
assert(oakengine_marker_list_at(list, 2) == NULL);
assert(oakengine_marker_list_at(list, -1) == NULL);
int64_t in_num = 0, in_den = 0, out_num = 0, out_den = 0;
assert(oakengine_marker_get_time(m0, &in_num, &in_den, &out_num,
&out_den) == OAKENGINE_OK);
assert(in_num == 1 && in_den == 1 && out_num == 2 && out_den == 1);
char name[64];
assert(oakengine_marker_get_name(m0, name, sizeof(name)) == 2);
assert(strcmp(name, "In") == 0);
assert(oakengine_marker_get_color(m0) == 0);
assert(oakengine_marker_get_color(m1) == 2);
// Lookup by exact in-point.
assert(oakengine_marker_list_marker_at_time(list, 4, 1) == m1);
assert(oakengine_marker_list_marker_at_time(list, 5, 1) == NULL);
// Sibling check: m0 has a sibling at 4s (m1), none at 3s.
assert(oakengine_marker_has_sibling_at_time(m0, 4, 1) == 1);
assert(oakengine_marker_has_sibling_at_time(m0, 3, 1) == 0);
// Live (non-undo) resize, then the undoable commit with the old range.
assert(oakengine_marker_set_time_live(m0, 1, 1, 3, 1) == OAKENGINE_OK);
assert(oakengine_marker_get_time(m0, NULL, NULL, &out_num, &out_den) ==
OAKENGINE_OK);
assert(out_num == 3 && out_den == 1);
assert(oakengine_marker_commit_time(m0, 1, 1, 3, 1, 1, 1, 2, 1,
NULL) == OAKENGINE_OK);
// Undo restores the pre-commit (live) state is NOT reverted (the live
// edit was already applied; undo goes back to the old range).
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_marker_get_time(m0, NULL, NULL, &out_num, &out_den) ==
OAKENGINE_OK);
assert(out_num == 2 && out_den == 1);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_marker_get_time(m0, NULL, NULL, &out_num, &out_den) ==
OAKENGINE_OK);
assert(out_num == 3 && out_den == 1);
// Detached marker creation (used by the UI before adding to a list).
OakEngineMarker *detached = oakengine_marker_create(3, 5, 1, 7, 1,
"Detached");
assert(detached != NULL);
assert(oakengine_marker_get_color(detached) == 3);
assert(oakengine_marker_get_name(detached, name, sizeof(name)) == 8);
assert(strcmp(name, "Detached") == 0);
assert(oakengine_marker_list_count(list) == 2);
oakengine_marker_free(detached);
// Batch properties: recolor + rename both markers as ONE undo entry.
OakEngineMarker *both[2] = { m0, m1 };
assert(oakengine_marker_set_properties(both, 2, 7, "Same", 0, 0, 0, 0,
0, NULL) == OAKENGINE_OK);
assert(oakengine_marker_get_color(m0) == 7);
assert(oakengine_marker_get_color(m1) == 7);
assert(oakengine_marker_get_name(m0, name, sizeof(name)) >= 0);
assert(strcmp(name, "Same") == 0);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_marker_get_color(m0) == 0);
assert(oakengine_marker_get_color(m1) == 2);
assert(oakengine_marker_get_name(m0, name, sizeof(name)) >= 0);
assert(strcmp(name, "In") == 0);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
// Single-marker time move through the same batch call.
assert(oakengine_marker_set_properties(both, 1, -1, NULL, 1, 10, 1, 12,
1, NULL) == OAKENGINE_OK);
assert(oakengine_marker_get_time(m0, &in_num, NULL, &out_num, NULL) ==
OAKENGINE_OK);
assert(in_num == 10 && out_num == 12);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
// Remove with undo.
assert(oakengine_marker_remove(m1) == OAKENGINE_OK);
assert(oakengine_marker_list_count(list) == 1);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_marker_list_count(list) == 2);
// NULL safety.
assert(oakengine_marker_get_time(NULL, &in_num, NULL, NULL, NULL) ==
OAKENGINE_E_INVALID);
assert(oakengine_marker_get_name(NULL, name, sizeof(name)) ==
OAKENGINE_E_INVALID);
assert(oakengine_marker_get_color(NULL) == -1);
assert(oakengine_marker_has_sibling_at_time(NULL, 1, 1) == 0);
assert(oakengine_marker_set_time_live(NULL, 0, 1, 1, 1) ==
OAKENGINE_E_INVALID);
assert(oakengine_marker_list_add(NULL, 0, 1, 1, 1, "x", 0) ==
OAKENGINE_E_INVALID);
assert(oakengine_marker_remove(NULL) == OAKENGINE_E_INVALID);
assert(oakengine_marker_set_properties(NULL, 1, 0, NULL, 0, 0, 0, 0, 0,
NULL) == OAKENGINE_E_INVALID);
oakengine_project_free(project);
}
// ---- Workarea handle family (B4c) ----------------------------------------------
static void test_workarea_handle_family(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "Workarea");
assert(seq != NULL);
// Reset sentinels: k_reset_in is 0, k_reset_out is RATIONAL_MAX.
int64_t ri_num = 0, ri_den = 0, ro_num = 0, ro_den = 0;
oakengine_workarea_reset_in_out(&ri_num, &ri_den, &ro_num, &ro_den);
assert(ri_num == 0 && ri_den > 0);
assert(ro_num > 0 && ro_den > 0);
OakEngineWorkarea *wa =
oakengine_viewer_get_workarea_handle((OakEngineNode *)seq);
assert(wa != NULL);
assert(oakengine_viewer_get_workarea_handle(NULL) == NULL);
// A fresh workarea is disabled.
int enabled = -1;
assert(oakengine_workarea_get(wa, NULL, NULL, NULL, NULL, &enabled) ==
OAKENGINE_OK);
assert(enabled == 0);
// Undoable enable + range change.
assert(oakengine_workarea_set_enabled_undoable(wa, 1, NULL) ==
OAKENGINE_OK);
assert(oakengine_workarea_get(wa, NULL, NULL, NULL, NULL, &enabled) ==
OAKENGINE_OK);
assert(enabled == 1);
assert(oakengine_workarea_set_range_undoable(wa, 1, 2, 3, 2, ri_num,
ri_den, ro_num, ro_den,
NULL) == OAKENGINE_OK);
int64_t in_num = 0, in_den = 0, out_num = 0, out_den = 0;
assert(oakengine_workarea_get(wa, &in_num, &in_den, &out_num, &out_den,
NULL) == OAKENGINE_OK);
assert(in_num == 1 && in_den == 2 && out_num == 3 && out_den == 2);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_workarea_get(wa, &in_num, NULL, &out_num, NULL,
NULL) == OAKENGINE_OK);
assert(in_num == ri_num && out_num == ro_num);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_workarea_get(wa, NULL, NULL, NULL, NULL, &enabled) ==
OAKENGINE_OK);
assert(enabled == 0);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
// Standalone workarea: enabled-undoable degrades to a direct apply
// (no project owns it).
OakEngineWorkarea *over = oakengine_workarea_create();
assert(over != NULL);
assert(oakengine_workarea_set_enabled_undoable(over, 1, NULL) ==
OAKENGINE_OK);
assert(oakengine_workarea_get(over, NULL, NULL, NULL, NULL, &enabled) ==
OAKENGINE_OK);
assert(enabled == 1);
assert(oakengine_workarea_set_range(over, 0, 1, 5, 1) == OAKENGINE_OK);
assert(oakengine_workarea_get(over, NULL, NULL, &out_num, &out_den,
NULL) == OAKENGINE_OK);
assert(out_num == 5 && out_den == 1);
oakengine_workarea_free(over);
// NULL safety.
assert(oakengine_workarea_get(NULL, &in_num, NULL, NULL, NULL, NULL) ==
OAKENGINE_E_INVALID);
assert(oakengine_workarea_set_range(NULL, 0, 1, 1, 1) ==
OAKENGINE_E_INVALID);
assert(oakengine_workarea_set_enabled(NULL, 1) == OAKENGINE_E_INVALID);
assert(oakengine_workarea_set_range_undoable(NULL, 0, 1, 1, 1, 0, 1, 1,
1, NULL) ==
OAKENGINE_E_INVALID);
assert(oakengine_workarea_set_enabled_undoable(NULL, 1, NULL) ==
OAKENGINE_E_INVALID);
oakengine_project_free(project);
}
// ---- Clip input ids / media in / cache (B4c) -----------------------------------
static void test_clip_input_ids_and_media(void)
{
// Input id statics: non-null, distinct, and stable across calls.
const char *ids[] = { oakengine_clip_buffer_input_id(),
oakengine_clip_speed_input_id(),
oakengine_clip_reverse_input_id(),
oakengine_clip_maintain_audio_pitch_input_id(),
oakengine_clip_loop_mode_input_id(),
oakengine_clip_auto_cache_input_id() };
for (size_t i = 0; i < sizeof(ids) / sizeof(ids[0]); i++) {
assert(ids[i] != NULL && ids[i][0] != '\0');
for (size_t j = i + 1; j < sizeof(ids) / sizeof(ids[0]); j++) {
assert(strcmp(ids[i], ids[j]) != 0);
}
}
assert(strcmp(oakengine_clip_speed_input_id(), ids[1]) == 0);
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "ClipMedia");
assert(seq != NULL);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
0);
char path[4096];
demo_path(path, sizeof(path));
OakEngineFootage *footage =
oakengine_project_import_footage(project, path);
assert(footage != NULL);
OakEngineClip *clip = oakengine_sequence_add_footage_clip(
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 30, 0);
assert(clip != NULL);
// Media in-point: read via the range getter, write undoably.
int64_t media_in = -1;
assert(oakengine_clip_get_range(clip, NULL, NULL, &media_in) ==
OAKENGINE_OK);
assert(media_in == 0);
assert(oakengine_clip_set_media_in(clip, 5, 1) == OAKENGINE_OK);
assert(oakengine_clip_get_range(clip, NULL, NULL, &media_in) ==
OAKENGINE_OK);
assert(media_in == 5);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_clip_get_range(clip, NULL, NULL, &media_in) ==
OAKENGINE_OK);
assert(media_in == 0);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
// Non-undoable mode applies directly.
assert(oakengine_clip_set_media_in(clip, 2, 0) == OAKENGINE_OK);
assert(oakengine_clip_get_range(clip, NULL, NULL, &media_in) ==
OAKENGINE_OK);
assert(media_in == 2);
// Cache entry points: smoke calls (headless, no caches to speak of).
oakengine_clip_request_invalidate(clip, 0, 0, 0);
oakengine_clip_request_invalidate(clip, 1, 0, 30);
oakengine_clip_add_cache_passthrough(clip, clip);
oakengine_clip_discard_cache(clip);
// NULL safety.
assert(oakengine_clip_set_media_in(NULL, 0, 1) == OAKENGINE_E_INVALID);
oakengine_clip_request_invalidate(NULL, 0, 0, 0);
oakengine_clip_add_cache_passthrough(NULL, clip);
oakengine_clip_add_cache_passthrough(clip, NULL);
oakengine_clip_discard_cache(NULL);
assert(oakengine_block_is_enabled(NULL) == 0);
assert(oakengine_block_is_enabled((OakEngineBlock *)clip) == 1);
oakengine_footage_free(footage);
oakengine_project_free(project);
}
// ---- Track height helpers / default nodes (B4c) --------------------------------
static void test_track_height_helpers(void)
{
assert(oakengine_track_height_default() > 0.0);
// Round-trip through the pixel conversion.
const int px = oakengine_track_default_height_in_pixels();
assert(px > 0);
assert(oakengine_track_height_internal_to_pixels(
oakengine_track_height_pixels_to_internal(px)) == px);
assert(oakengine_track_height_internal_to_pixels(
oakengine_track_height_default()) == px);
}
static void test_add_default_nodes(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "Defaults");
assert(seq != NULL);
int video = -1, audio = -1;
assert(oakengine_sequence_track_count(seq, &video, &audio, NULL) ==
OAKENGINE_OK);
assert(video == 0 && audio == 0);
// Adds one video + one audio track as ONE undo entry.
assert(oakengine_sequence_add_default_nodes(seq) == OAKENGINE_OK);
assert(oakengine_sequence_track_count(seq, &video, &audio, NULL) ==
OAKENGINE_OK);
assert(video == 1 && audio == 1);
assert(oakengine_project_undo(project) == OAKENGINE_OK);
assert(oakengine_sequence_track_count(seq, &video, &audio, NULL) ==
OAKENGINE_OK);
assert(video == 0 && audio == 0);
assert(oakengine_project_redo(project) == OAKENGINE_OK);
assert(oakengine_sequence_track_count(seq, &video, &audio, NULL) ==
OAKENGINE_OK);
assert(video == 1 && audio == 1);
assert(oakengine_sequence_add_default_nodes(NULL) ==
OAKENGINE_E_INVALID);
oakengine_project_free(project);
}
// ---- clip_get_media_range_rational ------------------------------------------
static void test_clip_get_media_range_rational(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "MediaRange");
assert(seq != NULL);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) == 0);
char path[4096];
demo_path(path, sizeof(path));
OakEngineFootage *footage =
oakengine_project_import_footage(project, path);
assert(footage != NULL);
OakEngineClip *clip = oakengine_sequence_add_footage_clip(
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 30, 0);
assert(clip != NULL);
// NULL handle.
assert(oakengine_clip_get_media_range_rational(NULL, NULL, NULL, NULL,
NULL) ==
OAKENGINE_E_INVALID);
// Valid clip: media range should have a non-zero duration.
int64_t in_num = -1, in_den = -1, out_num = -1, out_den = -1;
assert(oakengine_clip_get_media_range_rational(clip, &in_num, &in_den,
&out_num, &out_den) ==
OAKENGINE_OK);
assert(in_num >= 0 && in_den > 0 && out_num > in_num);
// Partial output pointers (any may be NULL).
assert(oakengine_clip_get_media_range_rational(clip, NULL, &in_den,
&out_num, NULL) ==
OAKENGINE_OK);
assert(oakengine_clip_get_media_range_rational(clip, NULL, NULL, NULL,
NULL) == OAKENGINE_OK);
oakengine_project_free(project);
}
// ---- clip_find_multicam / multicam_switch_source (basic) --------------------
static void test_multicam_basic(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
// clip_find_multicam: NULL clip returns NULL.
assert(oakengine_clip_find_multicam(NULL) == NULL);
// A non-clip node (Solid) returns NULL.
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
assert(oakengine_clip_find_multicam(solid) == NULL);
// multicam_switch_source: NULL args.
assert(oakengine_multicam_switch_source(NULL, NULL, 0, 0, 0.0, NULL) ==
OAKENGINE_E_INVALID);
assert(oakengine_project_remove_node(project, solid) == OAKENGINE_OK);
oakengine_project_free(project);
}
// ---- marker_list_add_existing -----------------------------------------------
static void test_marker_list_add_existing(void)
{
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "MarkerAdopt");
assert(seq != NULL);
OakEngineMarkerList *list =
oakengine_viewer_get_marker_list((OakEngineNode *)seq);
assert(list != NULL);
// Add a marker to the list, get its handle.
assert(oakengine_marker_list_add(list, 0, 1, 2, 1, "Test", 0) ==
OAKENGINE_OK);
OakEngineMarker *marker = oakengine_marker_list_at(list, 0);
assert(marker != NULL);
// Remove it from the list.
assert(oakengine_marker_remove(marker) == OAKENGINE_OK);
assert(oakengine_marker_list_count(list) == 0);
// add_existing to re-add it.
assert(oakengine_marker_list_add_existing(list, marker) ==
OAKENGINE_OK);
assert(oakengine_marker_list_count(list) == 1);
// NULL list or marker.
assert(oakengine_marker_list_add_existing(NULL, marker) ==
OAKENGINE_E_INVALID);
assert(oakengine_marker_list_add_existing(list, NULL) ==
OAKENGINE_E_INVALID);
oakengine_project_free(project);
}
int main(void)
{
make_tmpdir();
@@ -1237,6 +1828,16 @@ int main(void)
test_batch_editing(path);
test_batch_editing_round2(path);
test_batch_editing_round3(path);
test_sequence_clip();
test_track_queries(path);
test_marker_handle_family();
test_workarea_handle_family();
test_clip_input_ids_and_media();
test_track_height_helpers();
test_add_default_nodes();
test_clip_get_media_range_rational();
test_multicam_basic();
test_marker_list_add_existing();
oakengine_project_free(project);
assert(oakengine_shutdown() == OAKENGINE_OK);
+277
View File
@@ -0,0 +1,277 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the liboakengine traverse facade
// (oakengine/traverse.h) plus the node value-hint write path
// (oakengine_node_set_value_hint()). Builds a small node graph with the
// facade node family and exercises generate_database/generate_table, the db
// accessors, element_index_for_hint, generate_row's C-side error paths and
// transform. No GL required: evaluation is synchronous and CPU-only
// (textures resolve as engine-side dummy textures), so no GL gating is
// needed.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
#include <direct.h>
#include <windows.h>
#else
#include <unistd.h>
#endif
#include "oakengine/init.h"
#include "oakengine/node.h"
#include "oakengine/project.h"
#include "oakengine/traverse.h"
static char g_tmpdir[4096];
static void make_tmpdir(void)
{
#if defined(_WIN32)
char base[MAX_PATH];
const DWORD len = GetTempPathA(MAX_PATH, base);
assert(len > 0 && len < MAX_PATH);
snprintf(g_tmpdir, sizeof(g_tmpdir), "%soakengine_traverse_test_%lu", base,
(unsigned long)GetCurrentProcessId());
assert(_mkdir(g_tmpdir) == 0);
#else
strcpy(g_tmpdir, "/tmp/oakengine_traverse_test_XXXXXX");
assert(mkdtemp(g_tmpdir) != NULL);
#endif
}
// ---- Robustness: NULL/invalid arguments ------------------------------------
static void test_null_robustness(OakEngineNode *solid)
{
double m[6];
assert(oakengine_traverse_generate_database(NULL, 0, 1, 1, 1) == NULL);
assert(oakengine_traverse_generate_table(NULL, 0, 1, 1, 1) == NULL);
// Zero denominators are invalid rationals.
assert(oakengine_traverse_generate_database(solid, 0, 0, 1, 1) == NULL);
assert(oakengine_traverse_generate_table(solid, 0, 1, 1, 0) == NULL);
oakengine_traverse_db_free(NULL); // no-op
assert(oakengine_traverse_db_input_count(NULL) == 0);
assert(oakengine_traverse_db_input_id(NULL, 0) == NULL);
assert(oakengine_traverse_db_row_count(NULL, 0) == 0);
assert(oakengine_traverse_row_type(NULL, 0, 0) == OAK_NODE_VALUE_NONE);
assert(oakengine_traverse_row_source(NULL, 0, 0) == NULL);
assert(oakengine_traverse_row_tag(NULL, 0, 0) != NULL); // never NULL
assert(oakengine_traverse_row_value_string(NULL, 0, 0) == NULL);
assert(oakengine_traverse_row_split_count(NULL, 0, 0) == 0);
assert(oakengine_traverse_row_split_string(NULL, 0, 0, 0) == NULL);
assert(oakengine_traverse_table_element_index_for_hint(NULL, "x", -1,
NULL) == -1);
// generate_row: the C side can only exercise the error paths -- the
// real output is an olive::NodeValueRow (a C++ QHash typedef), which a
// pure C test cannot allocate. The filled-row path is covered by the
// application (the viewer display gizmo drag-start path).
assert(oakengine_traverse_generate_row(NULL, 0, 1, 1, 1, NULL, 0, 0,
(void *)1) == OAKENGINE_E_INVALID);
assert(oakengine_traverse_generate_row(solid, 0, 1, 1, 1, NULL, 0, 0,
NULL) == OAKENGINE_E_INVALID);
assert(oakengine_traverse_transform(NULL, solid, 0, 1, 1, 1, NULL, m) ==
OAKENGINE_E_INVALID);
assert(oakengine_traverse_transform(solid, NULL, 0, 1, 1, 1, NULL, m) ==
OAKENGINE_E_INVALID);
assert(oakengine_traverse_transform(solid, solid, 0, 1, 1, 1, NULL,
NULL) == OAKENGINE_E_INVALID);
assert(oakengine_node_set_value_hint(NULL, "x", -1, OAK_NODE_VALUE_COLOR,
0, NULL) == OAKENGINE_E_INVALID);
assert(oakengine_node_set_value_hint(solid, NULL, -1,
OAK_NODE_VALUE_COLOR, 0,
NULL) == OAKENGINE_E_INVALID);
}
// ---- generate_database + accessors ------------------------------------------
static void test_database(OakEngineNode *solid)
{
char buf[256];
OakEngineTraverseDb *db =
oakengine_traverse_generate_database(solid, 0, 1, 1, 1);
assert(db != NULL);
// One entry per node input, in the node's input order (deterministic).
const int node_inputs = oakengine_node_input_count(solid);
assert(node_inputs >= 2);
assert(oakengine_traverse_db_input_count(db) == node_inputs);
for (int i = 0; i < node_inputs; i++) {
assert(oakengine_node_input_id(solid, i, buf, sizeof(buf)) > 0);
const char *id = oakengine_traverse_db_input_id(db, i);
assert(id != NULL);
assert(strcmp(id, buf) == 0);
// Every input of an unconnected Solid generator produces one row
// (its standard value).
const int rows = oakengine_traverse_db_row_count(db, i);
assert(rows >= 1);
for (int r = 0; r < rows; r++) {
// Type is a valid facade value type for plain inputs
// (enabled_in is BOOL, color_in is COLOR).
const int type = oakengine_traverse_row_type(db, i, r);
assert(type > OAK_NODE_VALUE_NONE);
// Source: the value's originating node, or NULL; the solid's
// standard values are sourced from the node itself.
OakEngineNode *src = oakengine_traverse_row_source(db, i, r);
assert(src == NULL || src == solid);
// Tag may be empty but never NULL.
assert(oakengine_traverse_row_tag(db, i, r) != NULL);
// Value string is non-empty for these value types.
const char *vs = oakengine_traverse_row_value_string(db, i, r);
assert(vs != NULL);
assert(vs[0] != '\0');
// Split values: at least one track, each with a string.
const int splits = oakengine_traverse_row_split_count(db, i, r);
assert(splits >= 1);
for (int s = 0; s < splits; s++) {
assert(oakengine_traverse_row_split_string(db, i, r, s) !=
NULL);
}
assert(oakengine_traverse_row_split_string(db, i, r, splits) ==
NULL);
}
}
// Out-of-range accessors fail cleanly.
assert(oakengine_traverse_db_input_id(db, node_inputs) == NULL);
assert(oakengine_traverse_db_row_count(db, node_inputs) == 0);
assert(oakengine_traverse_row_type(db, node_inputs, 0) ==
OAK_NODE_VALUE_NONE);
// A multi-entry database is not a generate_table result: the hint
// lookup rejects it.
assert(oakengine_traverse_table_element_index_for_hint(solid, "color_in",
-1, db) == -1);
oakengine_traverse_db_free(db);
}
// ---- generate_table + element_index_for_hint + set_value_hint -----------------
static void test_table_and_hints(OakEngineNode *solid, OakEngineNode *lut)
{
OakEngineTraverseDb *db =
oakengine_traverse_generate_table(solid, 0, 1, 1, 1);
assert(db != NULL);
assert(oakengine_traverse_db_input_count(db) == 1);
// The single output table is keyed by an empty input id.
const char *id = oakengine_traverse_db_input_id(db, 0);
assert(id != NULL);
assert(id[0] == '\0');
assert(oakengine_traverse_db_row_count(db, 0) >= 1);
// set_value_hint: unknown input ids and bogus types are rejected.
assert(oakengine_node_set_value_hint(solid, "not_an_input", -1,
OAK_NODE_VALUE_COLOR, 0,
NULL) == OAKENGINE_E_NOT_FOUND);
assert(oakengine_node_set_value_hint(solid, "color_in", -1, 999, 0,
NULL) == OAKENGINE_E_INVALID);
// The solid's output table holds texture rows. A hint preferring COLOR
// values (set on the lut's texture input) matches nothing -> -1.
assert(oakengine_node_set_value_hint(lut, "tex_in", -1,
OAK_NODE_VALUE_COLOR, -1,
NULL) == OAKENGINE_OK);
assert(oakengine_traverse_table_element_index_for_hint(lut, "tex_in", -1,
db) == -1);
// An untyped hint falls back to the input's declared type (k_texture
// for "tex_in"), which does have a row in the table.
assert(oakengine_node_set_value_hint(lut, "tex_in", -1,
OAK_NODE_VALUE_NONE, -1,
NULL) == OAKENGINE_OK);
assert(oakengine_traverse_table_element_index_for_hint(lut, "tex_in", -1,
db) >= 0);
oakengine_traverse_db_free(db);
}
// ---- transform ------------------------------------------------------------------
static void test_transform(OakEngineNode *solid, OakEngineNode *lut)
{
double m[6] = { 0, 0, 0, 0, 0, 0 };
// No transform-generating nodes between start and end: identity matrix.
assert(oakengine_traverse_transform(solid, solid, 0, 1, 1, 1, NULL, m) ==
OAKENGINE_OK);
assert(m[0] == 1.0 && m[1] == 0.0 && m[2] == 0.0 && m[3] == 1.0);
assert(m[4] == 0.0 && m[5] == 0.0);
// Same through an edge, with explicit cache params.
oak_video_params vp;
memset(&vp, 0, sizeof(vp));
assert(oakengine_video_params_make(&vp, 1920, 1080, 1001, 30000, 0, 1, 1,
0, 0, 1) == OAKENGINE_OK);
memset(m, 0, sizeof(m));
assert(oakengine_traverse_transform(solid, lut, 0, 1, 1, 1, &vp, m) ==
OAKENGINE_OK);
assert(m[0] == 1.0 && m[1] == 0.0 && m[2] == 0.0 && m[3] == 1.0);
assert(m[4] == 0.0 && m[5] == 0.0);
}
int main(void)
{
make_tmpdir();
// Sandbox the config/cache/data locations.
#if !defined(_WIN32)
assert(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0);
assert(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0);
assert(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0);
#endif
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
OakEngineNode *lut = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.ociolut");
assert(lut != NULL);
test_null_robustness(solid);
test_database(solid);
test_table_and_hints(solid, lut);
test_transform(solid, lut);
oakengine_project_free(project);
assert(oakengine_shutdown() == OAKENGINE_OK);
printf("oakengine_traverse_test: all assertions passed\n");
return 0;
}
+592
View File
@@ -0,0 +1,592 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the liboakengine viewer facade (oakengine/viewer.h)
// and the viewer events (oakengine/events.h ids 100-110). Exercises every
// function of the family on a Sequence (a ViewerOutput subclass): handle
// validation, input ids, playhead/length, stream params, enabled streams,
// workarea, parameter setup, waveform and the change notifications. No GL
// required (headless init, CPU only). Uses tests/demo.mp4 to give the
// sequence real content length.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
#include <direct.h>
#include <windows.h>
#else
#include <unistd.h>
#endif
#include "oakengine/events.h"
#include "oakengine/footage.h"
#include "oakengine/init.h"
#include "oakengine/node.h"
#include "oakengine/project.h"
#include "oakengine/timeline.h"
#include "oakengine/viewer.h"
#ifndef OAK_TEST_SOURCE_DIR
#define OAK_TEST_SOURCE_DIR "."
#endif
static char g_tmpdir[4096];
static void make_tmpdir(void)
{
#if defined(_WIN32)
char base[MAX_PATH];
const DWORD len = GetTempPathA(MAX_PATH, base);
assert(len > 0 && len < MAX_PATH);
snprintf(g_tmpdir, sizeof(g_tmpdir), "%soakengine_viewer_test_%lu", base,
(unsigned long)GetCurrentProcessId());
assert(_mkdir(g_tmpdir) == 0);
#else
strcpy(g_tmpdir, "/tmp/oakengine_viewer_test_XXXXXX");
assert(mkdtemp(g_tmpdir) != NULL);
#endif
}
static void demo_path(char *dst, size_t cap)
{
const int n = snprintf(dst, cap, "%s/tests/demo.mp4", OAK_TEST_SOURCE_DIR);
assert(n > 0 && (size_t)n < cap);
}
// A sequence handle is the same engine object pointer as its node handle
// (all facade handles are reinterpreted engine pointers; see the wrap()
// helpers in src/capi/timeline.cpp).
static OakEngineNode *as_node(OakEngineSequence *seq)
{
return (OakEngineNode *)seq;
}
// ---- Handle validation / constants ----------------------------------------
static void test_from_node(OakEngineProject *project, OakEngineSequence *seq)
{
OakEngineNode *seq_node = as_node(seq);
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
assert(oakengine_viewer_from_node(NULL) == NULL);
assert(oakengine_viewer_from_node(solid) == NULL);
assert(oakengine_viewer_from_node(seq_node) == seq_node);
assert(oakengine_viewer_from_const_node(NULL) == NULL);
assert(oakengine_viewer_from_const_node((const OakEngineNode *)solid) ==
NULL);
assert(oakengine_viewer_from_const_node((const OakEngineNode *)seq_node) ==
(const OakEngineNode *)seq_node);
// The input id constants are static, non-empty strings.
assert(oakengine_viewer_video_params_input_id() != NULL);
assert(oakengine_viewer_video_params_input_id()[0] != '\0');
assert(oakengine_viewer_audio_params_input_id()[0] != '\0');
assert(oakengine_viewer_subtitle_params_input_id()[0] != '\0');
assert(oakengine_viewer_texture_input_id()[0] != '\0');
assert(oakengine_viewer_samples_input_id()[0] != '\0');
assert(oakengine_viewer_default_sample_format() >= 0);
}
// ---- Playhead / length ------------------------------------------------------
static void test_playhead(OakEngineSequence *seq)
{
int64_t num = -1, den = -1;
assert(oakengine_viewer_get_playhead(NULL, &num, &den) ==
OAKENGINE_E_INVALID);
assert(oakengine_viewer_get_playhead(as_node(seq), &num, &den) ==
OAKENGINE_OK);
assert(num == 0);
assert(oakengine_viewer_set_playhead(NULL, 1, 1) == OAKENGINE_E_INVALID);
assert(oakengine_viewer_set_playhead(as_node(seq), 2, 1) == OAKENGINE_OK);
num = den = -1;
assert(oakengine_viewer_get_playhead(as_node(seq), &num, &den) ==
OAKENGINE_OK);
assert(num == 2 && den == 1);
}
static void test_lengths(OakEngineSequence *seq)
{
int64_t num = -1, den = -1;
assert(oakengine_viewer_get_length(NULL, &num, &den) ==
OAKENGINE_E_INVALID);
assert(oakengine_viewer_get_length(as_node(seq), &num, &den) ==
OAKENGINE_OK);
assert(num == 0);
assert(oakengine_viewer_get_video_length(as_node(seq), &num, &den) ==
OAKENGINE_OK);
assert(num == 0);
assert(oakengine_viewer_get_audio_length(as_node(seq), &num, &den) ==
OAKENGINE_OK);
assert(num == 0);
}
// ---- Stream parameters --------------------------------------------------------
static void test_stream_params(OakEngineSequence *seq)
{
const OakEngineNode *node = (const OakEngineNode *)as_node(seq);
oak_video_params vp;
int sr = -1, format = -1;
uint64_t layout = 1;
assert(oakengine_viewer_get_video_params(NULL, 0, &vp) ==
OAKENGINE_E_INVALID);
assert(oakengine_viewer_get_video_params(node, 0, NULL) ==
OAKENGINE_E_INVALID);
// A fresh sequence has one video and one audio stream, no subtitles.
assert(oakengine_viewer_get_video_stream_count(NULL) == 0);
assert(oakengine_viewer_get_video_stream_count(node) == 1);
assert(oakengine_viewer_get_audio_stream_count(node) == 1);
assert(oakengine_viewer_get_subtitle_stream_count(node) == 0);
// In-range video params come from the sequence defaults.
assert(oakengine_viewer_get_video_params(node, 0, &vp) == OAKENGINE_OK);
assert(vp.width > 0 && vp.height > 0);
assert(vp.time_base_num > 0 && vp.time_base_den > 0);
// Out-of-range yields a zeroed struct (documented in viewer.h).
memset(&vp, 0xFF, sizeof(vp));
assert(oakengine_viewer_get_video_params(node, 99, &vp) == OAKENGINE_OK);
assert(vp.width == 0 && vp.height == 0);
// Audio params; out-of-range yields 0/0/0.
assert(oakengine_viewer_get_audio_params(NULL, 0, &sr, &layout,
&format) == OAKENGINE_E_INVALID);
assert(oakengine_viewer_get_audio_params(node, 0, &sr, &layout,
&format) == OAKENGINE_OK);
assert(sr > 0 && layout != 0);
sr = -1;
layout = 1;
format = -1;
assert(oakengine_viewer_get_audio_params(node, 99, &sr, &layout,
&format) == OAKENGINE_OK);
assert(sr == 0 && layout == 0 && format == 0);
// Per-stream enabled flags: video/audio stream 0 are enabled by
// default; subtitle has no stream 0.
assert(oakengine_viewer_get_stream_enabled(NULL, OAKENGINE_TRACK_TYPE_VIDEO,
0) == OAKENGINE_E_INVALID);
assert(oakengine_viewer_get_stream_enabled(node, OAKENGINE_TRACK_TYPE_VIDEO,
0) == 1);
assert(oakengine_viewer_get_stream_enabled(node, OAKENGINE_TRACK_TYPE_AUDIO,
0) == 1);
assert(oakengine_viewer_get_stream_enabled(
node, OAKENGINE_TRACK_TYPE_SUBTITLE, 0) == 0);
assert(oakengine_viewer_get_stream_enabled(node, 99, 0) ==
OAKENGINE_E_INVALID);
// Subtitle access: no subtitle streams on a fresh sequence.
assert(oakengine_viewer_get_subtitle_count(NULL, 0) == OAKENGINE_E_INVALID);
assert(oakengine_viewer_get_subtitle_count(node, 0) == OAKENGINE_E_INVALID);
assert(oakengine_viewer_get_subtitle_at(NULL, 0, 0) == NULL);
assert(oakengine_viewer_get_subtitle_at(node, 0, 0) == NULL);
}
static void test_enabled_streams(OakEngineSequence *seq)
{
const OakEngineNode *node = (const OakEngineNode *)as_node(seq);
oak_video_params vp;
assert(oakengine_viewer_has_enabled_streams(NULL,
OAKENGINE_TRACK_TYPE_VIDEO) ==
0);
assert(oakengine_viewer_has_enabled_streams(node,
OAKENGINE_TRACK_TYPE_VIDEO) ==
1);
assert(oakengine_viewer_has_enabled_streams(node,
OAKENGINE_TRACK_TYPE_AUDIO) ==
1);
assert(oakengine_viewer_has_enabled_streams(
node, OAKENGINE_TRACK_TYPE_SUBTITLE) == 0);
assert(oakengine_viewer_has_enabled_streams(node, 99) == 0);
assert(oakengine_viewer_get_first_enabled_video_stream(NULL, &vp) ==
OAKENGINE_E_INVALID);
assert(oakengine_viewer_get_first_enabled_video_stream(node, &vp) ==
OAKENGINE_OK);
assert(vp.width > 0 && vp.height > 0);
// Enabled stream references: video:0 and audio:0.
assert(oakengine_viewer_get_enabled_stream_count(NULL) == 0);
const int count = oakengine_viewer_get_enabled_stream_count(node);
assert(count == 2);
// Query form (max = 0, NULL arrays) returns the total count.
assert(oakengine_viewer_get_enabled_streams(node, NULL, NULL, 0) == count);
int types[8];
int indices[8];
memset(types, -1, sizeof(types));
memset(indices, -1, sizeof(indices));
assert(oakengine_viewer_get_enabled_streams(node, types, indices, 8) ==
count);
int saw_video = 0, saw_audio = 0;
for (int i = 0; i < count; i++) {
assert(indices[i] == 0);
if (types[i] == OAKENGINE_TRACK_TYPE_VIDEO) {
saw_video = 1;
} else if (types[i] == OAKENGINE_TRACK_TYPE_AUDIO) {
saw_audio = 1;
} else {
assert(0); // unexpected stream type
}
}
assert(saw_video && saw_audio);
// A smaller max truncates the write but still returns the total.
types[0] = types[1] = -1;
indices[0] = indices[1] = -1;
assert(oakengine_viewer_get_enabled_streams(node, types, indices, 1) ==
count);
assert(types[0] != -1 && types[1] == -1);
}
// ---- Workarea ------------------------------------------------------------------
static void test_workarea(OakEngineSequence *seq)
{
OakEngineNode *node = as_node(seq);
oakengine_viewer_workarea wa;
assert(oakengine_viewer_get_workarea(NULL, &wa) == OAKENGINE_E_INVALID);
assert(oakengine_viewer_get_workarea(node, NULL) == OAKENGINE_E_INVALID);
memset(&wa, 0xFF, sizeof(wa));
assert(oakengine_viewer_get_workarea(node, &wa) == OAKENGINE_OK);
assert(wa.enabled == 0);
assert(oakengine_viewer_set_workarea_range(NULL, 0, 1, 1, 1) ==
OAKENGINE_E_INVALID);
assert(oakengine_viewer_set_workarea_range(node, 1, 1, 5, 1) ==
OAKENGINE_OK);
assert(oakengine_viewer_set_workarea_enabled(NULL, 1) ==
OAKENGINE_E_INVALID);
assert(oakengine_viewer_set_workarea_enabled(node, 1) == OAKENGINE_OK);
memset(&wa, 0, sizeof(wa));
assert(oakengine_viewer_get_workarea(node, &wa) == OAKENGINE_OK);
assert(wa.in_num == 1 && wa.in_den == 1);
assert(wa.out_num == 5 && wa.out_den == 1);
assert(wa.enabled == 1);
assert(oakengine_viewer_set_workarea_enabled(node, 0) == OAKENGINE_OK);
assert(oakengine_viewer_get_workarea(node, &wa) == OAKENGINE_OK);
assert(wa.enabled == 0);
}
// ---- Parameter setup / waveform -------------------------------------------------
static void test_parameter_setup(OakEngineProject *project,
OakEngineSequence *seq)
{
OakEngineNode *node = as_node(seq);
assert(oakengine_viewer_set_default_parameters(NULL) ==
OAKENGINE_E_INVALID);
assert(oakengine_viewer_set_default_parameters(node) == OAKENGINE_OK);
// set_parameters_from_footage accepts any viewer handles; a second
// sequence stands in for the footage array here.
OakEngineSequence *other = oakengine_sequence_new(project, "Other");
assert(other != NULL);
OakEngineNode *other_node = as_node(other);
assert(oakengine_viewer_set_parameters_from_footage(NULL, &other_node,
1) == OAKENGINE_E_INVALID);
assert(oakengine_viewer_set_parameters_from_footage(node, NULL, 1) ==
OAKENGINE_E_INVALID);
// An empty array is a valid no-op.
assert(oakengine_viewer_set_parameters_from_footage(node, NULL, 0) ==
OAKENGINE_OK);
// One invalid element rejects the whole call.
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
OakEngineNode *mixed[2] = { other_node, solid };
assert(oakengine_viewer_set_parameters_from_footage(node, mixed, 2) ==
OAKENGINE_E_INVALID);
// All viewers: OK, and the params are adopted.
OakEngineNode *viewers[1] = { other_node };
assert(oakengine_viewer_set_parameters_from_footage(node, viewers, 1) ==
OAKENGINE_OK);
// Waveform toggle; nothing is connected to the samples input, so the
// connected waveform is NULL.
assert(oakengine_viewer_set_waveform_enabled(NULL, 1) ==
OAKENGINE_E_INVALID);
assert(oakengine_viewer_set_waveform_enabled(node, 1) == OAKENGINE_OK);
assert(oakengine_viewer_set_waveform_enabled(node, 0) == OAKENGINE_OK);
assert(oakengine_viewer_get_connected_waveform(NULL) == NULL);
assert(oakengine_viewer_get_connected_waveform(
(const OakEngineNode *)node) == NULL);
}
// ---- Events ---------------------------------------------------------------------
struct EventLog {
int playhead_events;
int64_t playhead_num;
int64_t playhead_den;
int length_events;
int64_t length_num;
int64_t length_den;
int size_events;
int64_t size_w;
int64_t size_h;
int video_params_events;
int audio_params_events;
int sample_rate_events;
int64_t sample_rate;
int texture_events;
int frame_rate_events;
int pixel_aspect_events;
int interlacing_events;
int64_t interlacing_mode;
int waveform_events;
};
static void record_event(const oakengine_event *event, void *userdata)
{
struct EventLog *log = (struct EventLog *)userdata;
assert(event != NULL);
switch (event->id) {
case OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED:
log->playhead_events++;
log->playhead_num = event->a;
log->playhead_den = event->b;
break;
case OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED:
log->length_events++;
log->length_num = event->a;
log->length_den = event->b;
break;
case OAKENGINE_EVENT_VIEWER_SIZE_CHANGED:
log->size_events++;
log->size_w = event->a;
log->size_h = event->b;
break;
case OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED:
log->video_params_events++;
break;
case OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED:
log->audio_params_events++;
break;
case OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED:
log->sample_rate_events++;
log->sample_rate = event->a;
break;
case OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED:
log->texture_events++;
break;
case OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED:
log->frame_rate_events++;
break;
case OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED:
log->pixel_aspect_events++;
break;
case OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED:
log->interlacing_events++;
log->interlacing_mode = event->a;
break;
case OAKENGINE_EVENT_VIEWER_CONNECTED_WAVEFORM_CHANGED:
log->waveform_events++;
break;
default:
assert(0); // unexpected event id on this subscription
}
}
static int64_t subscribe_checked(OakEngineNode *node, int32_t id,
struct EventLog *log)
{
const int64_t sub = oakengine_event_subscribe(node, id, record_event, log);
assert(sub > 0);
return sub;
}
static void test_events(OakEngineProject *project, OakEngineSequence *seq,
const char *media_path)
{
struct EventLog log;
memset(&log, 0, sizeof(log));
OakEngineNode *node = as_node(seq);
// Family mismatch: a viewer event on a non-viewer node must fail.
OakEngineNode *solid = oakengine_project_add_node(
project, "org.olivevideoeditor.Olive.solidgenerator");
assert(solid != NULL);
assert(oakengine_event_subscribe(solid,
OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED,
record_event, &log) == 0);
int64_t subs[16];
int n = 0;
subs[n++] = subscribe_checked(node, OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED,
&log);
subs[n++] =
subscribe_checked(node, OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED, &log);
subs[n++] = subscribe_checked(
node, OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED, &log);
subs[n++] =
subscribe_checked(node, OAKENGINE_EVENT_VIEWER_SIZE_CHANGED, &log);
subs[n++] = subscribe_checked(
node, OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED, &log);
subs[n++] = subscribe_checked(
node, OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED, &log);
subs[n++] = subscribe_checked(
node, OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED, &log);
subs[n++] = subscribe_checked(
node, OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED, &log);
subs[n++] = subscribe_checked(
node, OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED, &log);
subs[n++] = subscribe_checked(
node, OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED, &log);
subs[n++] = subscribe_checked(
node, OAKENGINE_EVENT_VIEWER_CONNECTED_WAVEFORM_CHANGED, &log);
// Playhead.
assert(oakengine_viewer_set_playhead(node, 3, 1) == OAKENGINE_OK);
assert(log.playhead_events == 1);
assert(log.playhead_num == 3 && log.playhead_den == 1);
// Length: placing a real clip makes verify_length() emit
// length_changed with the new content length.
OakEngineFootage *footage =
oakengine_project_import_footage(project, media_path);
assert(footage != NULL);
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) == 0);
OakEngineClip *clip = oakengine_sequence_add_footage_clip(
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 30, 0);
assert(clip != NULL);
assert(log.length_events >= 1);
assert(log.length_num > 0 && log.length_den > 0);
int64_t len_num = -1, len_den = -1;
assert(oakengine_viewer_get_length(node, &len_num, &len_den) ==
OAKENGINE_OK);
assert(len_num == log.length_num && len_den == log.length_den);
// Video params: changing the size emits size_changed (with the new
// dimensions as a/b) and video_params_changed.
assert(oakengine_sequence_set_video_params(seq, 1280, 720, -1, -1, -1, -1,
-1, -1, 0) == OAKENGINE_OK);
assert(log.size_events == 1);
assert(log.size_w == 1280 && log.size_h == 720);
assert(log.video_params_events == 1);
assert(log.frame_rate_events == 0);
assert(log.pixel_aspect_events == 0);
assert(log.interlacing_events == 0);
// Pixel aspect and interlacing changes fire their own events.
assert(oakengine_sequence_set_video_params(seq, -1, -1, -1, -1, 4, 3, -1,
-1, 0) == OAKENGINE_OK);
assert(log.pixel_aspect_events == 1);
assert(oakengine_sequence_set_video_params(seq, -1, -1, -1, -1, -1, -1, 1,
-1, 0) == OAKENGINE_OK);
assert(log.interlacing_events == 1);
assert(log.interlacing_mode == 1);
// Audio params: a new sample rate emits sample_rate_changed (a = rate)
// and audio_params_changed.
assert(oakengine_sequence_set_audio_params(seq, 44100, 0, 0) ==
OAKENGINE_OK);
assert(log.sample_rate_events == 1);
assert(log.sample_rate == 44100);
assert(log.audio_params_events == 1);
// Texture input: the placed clip's track auto-connected the viewer's
// texture input, so disconnect it first, then connect a node and check
// that texture_input_changed fired.
assert(oakengine_node_disconnect(node,
oakengine_viewer_texture_input_id()) ==
OAKENGINE_OK);
assert(oakengine_node_connect(solid, node,
oakengine_viewer_texture_input_id()) ==
OAKENGINE_OK);
assert(log.texture_events >= 1);
assert(oakengine_node_disconnect(node,
oakengine_viewer_texture_input_id()) ==
OAKENGINE_OK);
// connected_waveform_changed requires a connected sample output with a
// waveform cache; there is no audio-producing node chain in this test,
// so only the subscription itself is exercised above.
while (n > 0) {
assert(oakengine_event_unsubscribe(subs[--n]) == OAKENGINE_OK);
}
}
int main(void)
{
make_tmpdir();
// Sandbox the config/cache/data locations.
#if !defined(_WIN32)
assert(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0);
assert(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0);
assert(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0);
#endif
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
OakEngineProject *project = oakengine_project_create();
assert(project != NULL);
assert(oakengine_project_new(project) == OAKENGINE_OK);
OakEngineSequence *seq = oakengine_sequence_new(project, "ViewerSeq");
assert(seq != NULL);
test_from_node(project, seq);
test_playhead(seq);
test_lengths(seq);
test_stream_params(seq);
test_enabled_streams(seq);
test_workarea(seq);
test_parameter_setup(project, seq);
char media[4096];
demo_path(media, sizeof(media));
// test_parameter_setup() called set_default_parameters(), which reads
// the (empty, sandboxed) user config and may leave invalid params
// behind (same hazard oakengine_sequence_new() backfills against).
// Restore known-good params so the clip/timebase paths work.
assert(oakengine_sequence_set_video_params(seq, 1920, 1080, 30000, 1001,
1, 1, 0, -1, 0) == OAKENGINE_OK);
assert(oakengine_sequence_set_audio_params(seq, 48000, 3, 0) ==
OAKENGINE_OK);
test_events(project, seq, media);
oakengine_project_free(project);
assert(oakengine_shutdown() == OAKENGINE_OK);
printf("oakengine_viewer_test: all assertions passed\n");
return 0;
}
+237
View File
@@ -0,0 +1,237 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
// Pure C ABI test for the liboakengine render-worker facade
// (oakengine/worker.h). Exercises the session state machine without a
// renderer ("none" backend): create/destroy, malformed and unknown control
// messages, handshake validation, message ordering errors and shutdown
// idempotency. No GPU and no QApplication required: every path exercised
// here is a validation/error path that never touches a render backend.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "oakengine/worker.h"
// Handle one line into a heap buffer sized via the buf/size query
// convention. Returns the response (empty string when the message has no
// reply); the caller frees it. Asserts the query/fill round-trip agrees.
static char *handle(OakWorkerSession *session, const char *line)
{
const int needed =
oakengine_worker_session_handle_json(session, line, NULL, 0);
assert(needed >= 0);
char *buf = static_cast<char *>(malloc(size_t(needed) + 1));
const int written = oakengine_worker_session_handle_json(
session, line, buf, needed + 1);
assert(written == needed);
buf[needed] = '\0';
return buf;
}
static void assert_is_error_with(const char *response, const char *needle)
{
if (!strstr(response, "\"type\":\"error\"") ||
!strstr(response, needle)) {
fprintf(stderr,
"expected error response containing \"%s\", got: %s\n",
needle, response);
assert(0);
}
}
static void test_create_destroy(void)
{
// NULL, "" and "none" all skip renderer creation
const char *backends[] = { NULL, "", "none", "NONE" };
for (size_t i = 0; i < sizeof(backends) / sizeof(backends[0]); ++i) {
OakWorkerSession *s = oakengine_worker_session_create(backends[i]);
assert(s);
assert(oakengine_worker_session_has_renderer(s) == 0);
assert(oakengine_worker_session_shutdown_requested(s) == 0);
oakengine_worker_session_free(s);
}
// NULL tolerance
oakengine_worker_session_free(NULL);
assert(oakengine_worker_session_has_renderer(NULL) == 0);
assert(oakengine_worker_session_shutdown_requested(NULL) == 0);
assert(oakengine_worker_session_handle_json(NULL, "{}", NULL, 0) == -1);
}
static void test_startup_handshake(void)
{
OakWorkerSession *s = oakengine_worker_session_create("none");
assert(s);
// buf/size query convention
const int needed =
oakengine_worker_session_startup_handshake(s, NULL, 0);
assert(needed > 0);
char *buf = static_cast<char *>(malloc(size_t(needed) + 1));
assert(oakengine_worker_session_startup_handshake(s, buf, needed + 1) ==
needed);
buf[needed] = '\0';
assert(strstr(buf, "\"type\":\"handshake\""));
assert(strstr(buf, "\"protocol_version\":1"));
// No renderer -> no GL version announced
assert(!strstr(buf, "gl_major"));
free(buf);
assert(oakengine_worker_session_startup_handshake(NULL, NULL, 0) == -1);
oakengine_worker_session_free(s);
}
static void test_initialize_runtime(void)
{
// NULL tolerance
assert(oakengine_worker_session_initialize_runtime(NULL) == 0);
// Runtime init (EngineCore, factories, managers) must succeed without a
// renderer; the session stays usable for control messages afterwards.
OakWorkerSession *s = oakengine_worker_session_create("none");
assert(s);
assert(oakengine_worker_session_initialize_runtime(s) == 1);
char *r = handle(s, "{\"type\":\"teleport\"}");
assert_is_error_with(r, "unknown message type");
free(r);
oakengine_worker_session_free(s);
}
static void test_malformed_json(void)
{
OakWorkerSession *s = oakengine_worker_session_create("none");
char *r = handle(s, "{not json at all");
assert_is_error_with(r, "malformed control message");
free(r);
r = handle(s, "[1,2,3]");
assert_is_error_with(r, "malformed control message");
free(r);
oakengine_worker_session_free(s);
}
static void test_unknown_type(void)
{
OakWorkerSession *s = oakengine_worker_session_create("none");
char *r = handle(s, "{\"type\":\"teleport\"}");
assert_is_error_with(r, "unknown message type: teleport");
free(r);
oakengine_worker_session_free(s);
}
static void test_handshake_validation(void)
{
OakWorkerSession *s = oakengine_worker_session_create("none");
// Protocol version mismatch is rejected before any shm access
char *r = handle(s,
"{\"type\":\"handshake\",\"protocol_version\":999,"
"\"shm_key\":\"x\",\"output_slots\":1,"
"\"slot_data_bytes\":16}");
assert_is_error_with(r, "unsupported protocol version 999");
free(r);
// Matching version but no shared-memory geometry
r = handle(s, "{\"type\":\"handshake\",\"protocol_version\":1}");
assert_is_error_with(r, "missing output shared-memory geometry");
free(r);
oakengine_worker_session_free(s);
}
static void test_render_frame_before_load_graph(void)
{
OakWorkerSession *s = oakengine_worker_session_create("none");
char *r = handle(s,
"{\"type\":\"render_frame\",\"ticket\":7,"
"\"node_uuid\":\"abc\",\"time_num\":0,\"time_den\":1}");
assert_is_error_with(r, "render_frame received before load_graph");
// The error carries the ticket id so the caller can correlate
assert(strstr(r, "\"ticket\":7"));
free(r);
oakengine_worker_session_free(s);
}
static void test_load_graph_missing_file(void)
{
OakWorkerSession *s = oakengine_worker_session_create("none");
char *r = handle(s,
"{\"type\":\"load_graph\","
"\"path\":\"/nonexistent/definitely/missing.ove\"}");
assert_is_error_with(r, "graph file does not exist");
free(r);
// A failed load must not arm the session: render_frame still complains
// about the missing graph, not about the shm handshake order
r = handle(s,
"{\"type\":\"render_frame\",\"ticket\":1,"
"\"node_uuid\":\"abc\",\"time_num\":0,\"time_den\":1}");
assert_is_error_with(r, "render_frame received before load_graph");
free(r);
oakengine_worker_session_free(s);
}
static void test_shutdown_idempotent(void)
{
OakWorkerSession *s = oakengine_worker_session_create("none");
// Shutdown produces no response and latches the flag
char *r = handle(s, "{\"type\":\"shutdown\"}");
assert(r[0] == '\0');
free(r);
assert(oakengine_worker_session_shutdown_requested(s) == 1);
// Repeating it is a harmless no-op
r = handle(s, "{\"type\":\"shutdown\"}");
assert(r[0] == '\0');
free(r);
assert(oakengine_worker_session_shutdown_requested(s) == 1);
// The session still answers other messages afterwards
r = handle(s, "{\"type\":\"teleport\"}");
assert_is_error_with(r, "unknown message type");
free(r);
oakengine_worker_session_free(s);
}
static void test_cancel_is_silent(void)
{
OakWorkerSession *s = oakengine_worker_session_create("none");
char *r = handle(s, "{\"type\":\"cancel\",\"ticket\":3}");
assert(r[0] == '\0');
free(r);
oakengine_worker_session_free(s);
}
int main(void)
{
test_create_destroy();
test_startup_handshake();
test_initialize_runtime();
test_malformed_json();
test_unknown_type();
test_handshake_validation();
test_render_frame_before_load_graph();
test_load_graph_missing_file();
test_shutdown_idempotent();
test_cancel_is_silent();
return 0;
}