/*** 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 . ***/ // Pure C ABI tests for the liboakengine task and undo families // (oakengine/task.h and oakengine/undo.h). Runs headless; no GPU required. #include #include #include #include #include #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; }