/*** 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 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 #include #include #include #include #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); EXPECT_TRUE(needed >= 0); char *buf = static_cast(malloc(size_t(needed) + 1)); const int written = oakengine_worker_session_handle_json( session, line, buf, needed + 1); EXPECT_TRUE(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); EXPECT_TRUE(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]); EXPECT_TRUE(s); EXPECT_TRUE(oakengine_worker_session_has_renderer(s) == 0); EXPECT_TRUE(oakengine_worker_session_shutdown_requested(s) == 0); oakengine_worker_session_free(s); } // NULL tolerance oakengine_worker_session_free(NULL); EXPECT_TRUE(oakengine_worker_session_has_renderer(NULL) == 0); EXPECT_TRUE(oakengine_worker_session_shutdown_requested(NULL) == 0); EXPECT_TRUE(oakengine_worker_session_handle_json(NULL, "{}", NULL, 0) == -1); } static void test_startup_handshake(void) { OakWorkerSession *s = oakengine_worker_session_create("none"); EXPECT_TRUE(s); // buf/size query convention const int needed = oakengine_worker_session_startup_handshake(s, NULL, 0); EXPECT_TRUE(needed > 0); char *buf = static_cast(malloc(size_t(needed) + 1)); EXPECT_TRUE(oakengine_worker_session_startup_handshake(s, buf, needed + 1) == needed); buf[needed] = '\0'; EXPECT_TRUE(strstr(buf, "\"type\":\"handshake\"")); EXPECT_TRUE(strstr(buf, "\"protocol_version\":1")); // No renderer -> no GL version announced EXPECT_TRUE(!strstr(buf, "gl_major")); free(buf); EXPECT_TRUE(oakengine_worker_session_startup_handshake(NULL, NULL, 0) == -1); oakengine_worker_session_free(s); } static void test_initialize_runtime(void) { // NULL tolerance EXPECT_TRUE(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"); EXPECT_TRUE(s); EXPECT_TRUE(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 EXPECT_TRUE(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\"}"); EXPECT_TRUE(r[0] == '\0'); free(r); EXPECT_TRUE(oakengine_worker_session_shutdown_requested(s) == 1); // Repeating it is a harmless no-op r = handle(s, "{\"type\":\"shutdown\"}"); EXPECT_TRUE(r[0] == '\0'); free(r); EXPECT_TRUE(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}"); EXPECT_TRUE(r[0] == '\0'); free(r); oakengine_worker_session_free(s); } TEST(OakEngineWorker, Main) { 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(); }