238 lines
7.7 KiB
C++
238 lines
7.7 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 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 <gtest/gtest.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);
|
|
EXPECT_TRUE(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);
|
|
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<char *>(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();
|
|
}
|