Files
oak-editor/engine/tests/oakengine_app_test.cpp
T

511 lines
17 KiB
C++

/***
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 <gtest/gtest.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;
EXPECT_TRUE(userdata == &g_cb);
g_cb.confirm_image_sequence_calls++;
return 1;
}
static int on_relink_footage(OakEngineFootage **footage, int count,
void *userdata)
{
(void) footage;
EXPECT_TRUE(userdata == &g_cb);
g_cb.relink_calls++;
EXPECT_TRUE(count >= 0);
return 1;
}
static void on_save_project(const char *override_filename, void *userdata)
{
(void) override_filename;
EXPECT_TRUE(userdata == &g_cb);
g_cb.save_project_calls++;
}
static int on_close_project(void *userdata)
{
EXPECT_TRUE(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;
EXPECT_TRUE(userdata == &g_cb);
g_cb.load_layout_calls++;
}
static int on_otio_import(OakEngineSequence **sequences, int count,
void *userdata)
{
(void) sequences;
(void) count;
EXPECT_TRUE(userdata == &g_cb);
g_cb.otio_import_calls++;
return 1;
}
static void on_status_message_show(const char *message, int timeout,
void *userdata)
{
EXPECT_TRUE(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)
{
EXPECT_TRUE(userdata == &g_cb);
g_cb.status_clear_calls++;
}
static void on_cache_full_warning(void *userdata)
{
EXPECT_TRUE(userdata == &g_cb);
g_cb.cache_full_calls++;
}
static void on_active_project_changed(OakEngineProject *project,
void *userdata)
{
EXPECT_TRUE(userdata == &g_cb);
g_cb.active_project_calls++;
g_cb.last_project = project;
}
static void on_tool_changed(int tool, void *userdata)
{
EXPECT_TRUE(userdata == &g_cb);
g_cb.tool_changed_calls++;
g_cb.last_tool = tool;
}
static void on_addable_object_changed(int object, void *userdata)
{
EXPECT_TRUE(userdata == &g_cb);
g_cb.addable_changed_calls++;
g_cb.last_addable = object;
}
static void on_snapping_changed(int snapping, void *userdata)
{
EXPECT_TRUE(userdata == &g_cb);
g_cb.snapping_changed_calls++;
g_cb.last_snapping = snapping;
}
static void on_timecode_display_changed(int display, void *userdata)
{
EXPECT_TRUE(userdata == &g_cb);
g_cb.timecode_changed_calls++;
g_cb.last_display = display;
}
static void on_open_recent_list_changed(void *userdata)
{
EXPECT_TRUE(userdata == &g_cb);
g_cb.recent_changed_calls++;
}
static void on_color_picker_enabled(int enabled, void *userdata)
{
EXPECT_TRUE(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);
EXPECT_TRUE(needed >= 0);
char *buf = (char *) malloc(size_t(needed) + 1);
EXPECT_TRUE(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
EXPECT_TRUE(oakengine_app_create(&params) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_run_mode() == OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE);
EXPECT_TRUE(oakengine_app_fullscreen() == 1);
char *startup = query0(oakengine_app_startup_project);
EXPECT_TRUE(strcmp(startup, "/tmp/startup.ove") == 0);
free(startup);
// Only one application core may exist
EXPECT_TRUE(oakengine_app_create(NULL) == OAKENGINE_E_STATE);
}
static void test_start_stop(void)
{
// Not started yet
EXPECT_TRUE(oakengine_app_stop() == OAKENGINE_E_STATE);
// Full engine start (config, managers, autorecovery, recent list)
EXPECT_TRUE(oakengine_app_start() == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_start() == OAKENGINE_E_STATE);
EXPECT_TRUE(oakengine_app_stop() == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_stop() == OAKENGINE_E_STATE);
}
static void test_tool_state(void)
{
OakEngineAppCallbacks cb = make_callbacks();
EXPECT_TRUE(oakengine_app_set_callbacks(&cb) == OAKENGINE_OK);
// Tool (k_none=0 .. k_track_select=13, k_count=14)
EXPECT_TRUE(oakengine_app_set_tool(4) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_tool() == 4);
EXPECT_TRUE(g_cb.tool_changed_calls == 1 && g_cb.last_tool == 4);
EXPECT_TRUE(oakengine_app_set_tool(-1) == OAKENGINE_E_INVALID);
EXPECT_TRUE(oakengine_app_set_tool(999) == OAKENGINE_E_INVALID);
// Addable object
EXPECT_TRUE(oakengine_app_set_addable_object(2) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_addable_object() == 2);
EXPECT_TRUE(g_cb.addable_changed_calls == 1 && g_cb.last_addable == 2);
EXPECT_TRUE(oakengine_app_set_addable_object(999) == OAKENGINE_E_INVALID);
// Snapping
EXPECT_TRUE(oakengine_app_set_snapping(0) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_snapping() == 0);
EXPECT_TRUE(g_cb.snapping_changed_calls == 1 && g_cb.last_snapping == 0);
EXPECT_TRUE(oakengine_app_set_snapping(1) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_snapping() == 1);
EXPECT_TRUE(g_cb.snapping_changed_calls == 2 && g_cb.last_snapping == 1);
// Timecode display (0..4)
EXPECT_TRUE(oakengine_app_set_timecode_display(3) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_timecode_display() == 3);
EXPECT_TRUE(g_cb.timecode_changed_calls == 1 && g_cb.last_display == 3);
EXPECT_TRUE(oakengine_app_set_timecode_display(999) == OAKENGINE_E_INVALID);
// Selected transition
EXPECT_TRUE(oakengine_app_set_selected_transition("crossdissolve") ==
OAKENGINE_OK);
char *transition = query0(oakengine_app_selected_transition);
EXPECT_TRUE(strcmp(transition, "crossdissolve") == 0);
free(transition);
EXPECT_TRUE(oakengine_app_set_selected_transition(NULL) == OAKENGINE_OK);
transition = query0(oakengine_app_selected_transition);
EXPECT_TRUE(transition[0] == '\0');
free(transition);
// Magic flag
EXPECT_TRUE(oakengine_app_set_magic(1) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_is_magic_enabled() == 1);
EXPECT_TRUE(oakengine_app_set_magic(0) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_is_magic_enabled() == 0);
}
static void test_status_and_pixel_sampling(void)
{
EXPECT_TRUE(oakengine_app_show_status_message("hello", 250) == OAKENGINE_OK);
EXPECT_TRUE(g_cb.status_show_calls == 1);
EXPECT_TRUE(strcmp(g_cb.last_status, "hello") == 0);
EXPECT_TRUE(g_cb.last_timeout == 250);
EXPECT_TRUE(oakengine_app_show_status_message(NULL, 0) == OAKENGINE_E_INVALID);
EXPECT_TRUE(oakengine_app_clear_status_message() == OAKENGINE_OK);
EXPECT_TRUE(g_cb.status_clear_calls == 1);
// Pixel sampling ref-count emits only when crossing zero
EXPECT_TRUE(oakengine_app_request_pixel_sampling(1) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_request_pixel_sampling(1) == OAKENGINE_OK);
EXPECT_TRUE(g_cb.color_picker_calls == 1 && g_cb.last_color_picker == 1);
EXPECT_TRUE(oakengine_app_request_pixel_sampling(0) == OAKENGINE_OK);
EXPECT_TRUE(g_cb.color_picker_calls == 1);
EXPECT_TRUE(oakengine_app_request_pixel_sampling(0) == OAKENGINE_OK);
EXPECT_TRUE(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
EXPECT_TRUE(oakengine_app_create_new_project() == OAKENGINE_OK);
EXPECT_TRUE(g_cb.close_project_calls == 1);
OakEngineProject *p = oakengine_app_open_project();
EXPECT_TRUE(p != NULL);
EXPECT_TRUE(g_cb.active_project_calls > active_before);
EXPECT_TRUE(g_cb.last_project == p);
// Replacing it invokes the close handler again
EXPECT_TRUE(oakengine_app_create_new_project() == OAKENGINE_OK);
EXPECT_TRUE(g_cb.close_project_calls == 2);
p = oakengine_app_open_project();
EXPECT_TRUE(p != NULL);
// Saving a project without a filename keeps the recent list unchanged
EXPECT_TRUE(oakengine_app_clear_recent_projects() == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_on_project_saved(p) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_recent_projects_count() == 0);
// Detach and free it again
EXPECT_TRUE(oakengine_app_set_active_project(NULL) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_open_project() == NULL);
EXPECT_TRUE(g_cb.last_project == NULL);
oakengine_project_free(p);
// add_open_project adopts an externally created project
p = oakengine_project_create();
EXPECT_TRUE(p != NULL);
EXPECT_TRUE(oakengine_project_new(p) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_add_open_project(p, 0) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_open_project() == p);
EXPECT_TRUE(oakengine_app_set_active_project(NULL) == OAKENGINE_OK);
oakengine_project_free(p);
// NULL tolerance
EXPECT_TRUE(oakengine_app_add_open_project(NULL, 0) == OAKENGINE_E_INVALID);
EXPECT_TRUE(oakengine_app_on_project_saved(NULL) == OAKENGINE_E_INVALID);
EXPECT_TRUE(oakengine_app_add_open_project_from_task(NULL, 0) ==
OAKENGINE_E_INVALID);
EXPECT_TRUE(oakengine_app_add_recovery_project_from_task(NULL) ==
OAKENGINE_E_INVALID);
}
static void test_recent_projects(void)
{
// Start from a clean list
EXPECT_TRUE(oakengine_app_clear_recent_projects() == OAKENGINE_OK);
EXPECT_TRUE(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();
EXPECT_TRUE(p != NULL);
EXPECT_TRUE(oakengine_project_new(p) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_project_save(p, "oakengine_app_test_recent.ove") ==
OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_on_project_saved(p) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_recent_projects_count() == 1);
EXPECT_TRUE(g_cb.recent_changed_calls > changes_before);
const int needed = oakengine_app_recent_project_at(0, NULL, 0);
EXPECT_TRUE(needed > 0);
char *buf = (char *) malloc(size_t(needed) + 1);
EXPECT_TRUE(oakengine_app_recent_project_at(0, buf, needed + 1) == needed);
buf[needed] = '\0';
EXPECT_TRUE(strstr(buf, "oakengine_app_test_recent.ove") != NULL);
free(buf);
// Out-of-range access is rejected (the engine would assert otherwise)
EXPECT_TRUE(oakengine_app_recent_project_at(5, NULL, 0) ==
OAKENGINE_E_NOT_FOUND);
EXPECT_TRUE(oakengine_app_remove_recent_project(5) == OAKENGINE_E_NOT_FOUND);
EXPECT_TRUE(oakengine_app_remove_recent_project(0) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_recent_projects_count() == 0);
remove("oakengine_app_test_recent.ove");
oakengine_project_free(p);
}
static void test_clipboard(void)
{
EXPECT_TRUE(oakengine_app_copy_to_clipboard("hello clipboard") ==
OAKENGINE_OK);
char *text = query0(oakengine_app_paste_from_clipboard);
EXPECT_TRUE(strcmp(text, "hello clipboard") == 0);
free(text);
EXPECT_TRUE(oakengine_app_copy_to_clipboard(NULL) == OAKENGINE_E_INVALID);
}
static void test_footage_filter(void)
{
EXPECT_TRUE(oakengine_app_is_footage_extension_allowed("movie.mp4") == 1);
EXPECT_TRUE(oakengine_app_is_footage_extension_allowed("IMAGE.PNG") == 1);
EXPECT_TRUE(oakengine_app_is_footage_extension_allowed("doc.txt") == 0);
EXPECT_TRUE(oakengine_app_is_footage_extension_allowed(NULL) ==
OAKENGINE_E_INVALID);
char *filter = query0(oakengine_app_footage_file_dialog_filter);
EXPECT_TRUE(strstr(filter, "*.mp4") != NULL);
EXPECT_TRUE(strstr(filter, ";;") != NULL);
free(filter);
}
static void test_misc(void)
{
// Unknown locale is reported as "not found" without failing
EXPECT_TRUE(oakengine_app_set_language("definitely_not_a_locale_xx") == 0);
EXPECT_TRUE(oakengine_app_set_language(NULL) == OAKENGINE_E_INVALID);
EXPECT_TRUE(oakengine_app_set_autorecovery_interval(5) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_set_use_proxy_media(1) == OAKENGINE_OK);
char *index = query0(oakengine_app_auto_recovery_index_filename);
EXPECT_TRUE(strstr(index, "unrecovered") != NULL);
free(index);
EXPECT_TRUE(oakengine_app_undo_stack() != NULL);
}
static void test_create_sequence(void)
{
OakEngineProject *p = oakengine_project_create();
EXPECT_TRUE(p != NULL);
EXPECT_TRUE(oakengine_project_new(p) == OAKENGINE_OK);
OakEngineSequence *s = oakengine_app_create_sequence(p, "Seq %1");
EXPECT_TRUE(s != NULL);
EXPECT_TRUE(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)
{
EXPECT_TRUE(oakengine_app_set_callbacks(NULL) == OAKENGINE_OK);
// State changes still work, but nothing is delivered anymore
const int calls = g_cb.tool_changed_calls;
EXPECT_TRUE(oakengine_app_set_tool(2) == OAKENGINE_OK);
EXPECT_TRUE(oakengine_app_tool() == 2);
EXPECT_TRUE(g_cb.tool_changed_calls == calls);
}
TEST(OakEngineApp, Main)
{
// 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)
EXPECT_TRUE(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();
EXPECT_TRUE(oakengine_shutdown() == OAKENGINE_OK);
}