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:
@@ -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(¶ms) == 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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\"",
|
||||
|
||||
@@ -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(¶ms, 0, sizeof(params));
|
||||
assert(oakengine_footage_get_effective_proxy_params(f, ¶ms) ==
|
||||
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, ¶ms) ==
|
||||
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, ¶ms) ==
|
||||
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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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(¶ms, 0xFF, sizeof(params));
|
||||
|
||||
assert(oakengine_proxy_create_instance() == OAKENGINE_OK);
|
||||
assert(oakengine_proxy_params_from_config(¶ms) == 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;
|
||||
}
|
||||
@@ -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 ---------------------------------------------------
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user