- Track: new InputDisconnectedEvent trims blocks_/block_array_indexes_ when a block edge is removed outside the Track's own operations (block deletion, undo commands detaching a whole track). Previously blocks_ kept dangling pointers and Project teardown crashed in track_length() (ASan heap-use-after-free in oakengine_timeline_edit / oakengine_sync). The persistent array map is intentionally not rewritten so undo of remove_track can re-attach the clips; replace_block guards the handler with ignore_block_disconnect_ - filefunctions: honor OAK_CONFIG_DIR to redirect the configuration root; QStandardPaths ignores XDG_* on macOS, so the engine tests read the real user config (a stale 0.1.x file) and failed frame-rate assertions. All engine tests now set OAK_CONFIG_DIR to their tmpdir - init test: chdir to the fixture directory before loading project_with_footage.ove so the engine's moved-project footage relocation does not rewrite the stored relative filename - renderworkerpool: do not let the render worker inherit QT_QPA_PLATFORM=offscreen from a headless host process; the worker needs a real platform GL context and exited immediately otherwise, failing oak_cli_transcode
278 lines
10 KiB
C++
278 lines
10 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 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 <gtest/gtest.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);
|
|
EXPECT_TRUE(len > 0 && len < MAX_PATH);
|
|
snprintf(g_tmpdir, sizeof(g_tmpdir), "%soakengine_traverse_test_%lu", base,
|
|
(unsigned long)GetCurrentProcessId());
|
|
EXPECT_TRUE(_mkdir(g_tmpdir) == 0);
|
|
#else
|
|
strcpy(g_tmpdir, "/tmp/oakengine_traverse_test_XXXXXX");
|
|
EXPECT_TRUE(mkdtemp(g_tmpdir) != NULL);
|
|
#endif
|
|
}
|
|
|
|
// ---- Robustness: NULL/invalid arguments ------------------------------------
|
|
|
|
static void test_null_robustness(OakEngineNode *solid)
|
|
{
|
|
double m[6];
|
|
|
|
EXPECT_TRUE(oakengine_traverse_generate_database(NULL, 0, 1, 1, 1) == NULL);
|
|
EXPECT_TRUE(oakengine_traverse_generate_table(NULL, 0, 1, 1, 1) == NULL);
|
|
// Zero denominators are invalid rationals.
|
|
EXPECT_TRUE(oakengine_traverse_generate_database(solid, 0, 0, 1, 1) == NULL);
|
|
EXPECT_TRUE(oakengine_traverse_generate_table(solid, 0, 1, 1, 0) == NULL);
|
|
|
|
oakengine_traverse_db_free(NULL); // no-op
|
|
|
|
EXPECT_TRUE(oakengine_traverse_db_input_count(NULL) == 0);
|
|
EXPECT_TRUE(oakengine_traverse_db_input_id(NULL, 0) == NULL);
|
|
EXPECT_TRUE(oakengine_traverse_db_row_count(NULL, 0) == 0);
|
|
EXPECT_TRUE(oakengine_traverse_row_type(NULL, 0, 0) == OAK_NODE_VALUE_NONE);
|
|
EXPECT_TRUE(oakengine_traverse_row_source(NULL, 0, 0) == NULL);
|
|
EXPECT_TRUE(oakengine_traverse_row_tag(NULL, 0, 0) != NULL); // never NULL
|
|
EXPECT_TRUE(oakengine_traverse_row_value_string(NULL, 0, 0) == NULL);
|
|
EXPECT_TRUE(oakengine_traverse_row_split_count(NULL, 0, 0) == 0);
|
|
EXPECT_TRUE(oakengine_traverse_row_split_string(NULL, 0, 0, 0) == NULL);
|
|
|
|
EXPECT_TRUE(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).
|
|
EXPECT_TRUE(oakengine_traverse_generate_row(NULL, 0, 1, 1, 1, NULL, 0, 0,
|
|
(void *)1) == OAKENGINE_E_INVALID);
|
|
EXPECT_TRUE(oakengine_traverse_generate_row(solid, 0, 1, 1, 1, NULL, 0, 0,
|
|
NULL) == OAKENGINE_E_INVALID);
|
|
|
|
EXPECT_TRUE(oakengine_traverse_transform(NULL, solid, 0, 1, 1, 1, NULL, m) ==
|
|
OAKENGINE_E_INVALID);
|
|
EXPECT_TRUE(oakengine_traverse_transform(solid, NULL, 0, 1, 1, 1, NULL, m) ==
|
|
OAKENGINE_E_INVALID);
|
|
EXPECT_TRUE(oakengine_traverse_transform(solid, solid, 0, 1, 1, 1, NULL,
|
|
NULL) == OAKENGINE_E_INVALID);
|
|
|
|
EXPECT_TRUE(oakengine_node_set_value_hint(NULL, "x", -1, OAK_NODE_VALUE_COLOR,
|
|
0, NULL) == OAKENGINE_E_INVALID);
|
|
EXPECT_TRUE(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);
|
|
EXPECT_TRUE(db != NULL);
|
|
|
|
// One entry per node input, in the node's input order (deterministic).
|
|
const int node_inputs = oakengine_node_input_count(solid);
|
|
EXPECT_TRUE(node_inputs >= 2);
|
|
EXPECT_TRUE(oakengine_traverse_db_input_count(db) == node_inputs);
|
|
for (int i = 0; i < node_inputs; i++) {
|
|
EXPECT_TRUE(oakengine_node_input_id(solid, i, buf, sizeof(buf)) > 0);
|
|
const char *id = oakengine_traverse_db_input_id(db, i);
|
|
EXPECT_TRUE(id != NULL);
|
|
EXPECT_TRUE(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);
|
|
EXPECT_TRUE(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);
|
|
EXPECT_TRUE(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);
|
|
EXPECT_TRUE(src == NULL || src == solid);
|
|
// Tag may be empty but never NULL.
|
|
EXPECT_TRUE(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);
|
|
EXPECT_TRUE(vs != NULL);
|
|
EXPECT_TRUE(vs[0] != '\0');
|
|
// Split values: at least one track, each with a string.
|
|
const int splits = oakengine_traverse_row_split_count(db, i, r);
|
|
EXPECT_TRUE(splits >= 1);
|
|
for (int s = 0; s < splits; s++) {
|
|
EXPECT_TRUE(oakengine_traverse_row_split_string(db, i, r, s) !=
|
|
NULL);
|
|
}
|
|
EXPECT_TRUE(oakengine_traverse_row_split_string(db, i, r, splits) ==
|
|
NULL);
|
|
}
|
|
}
|
|
|
|
// Out-of-range accessors fail cleanly.
|
|
EXPECT_TRUE(oakengine_traverse_db_input_id(db, node_inputs) == NULL);
|
|
EXPECT_TRUE(oakengine_traverse_db_row_count(db, node_inputs) == 0);
|
|
EXPECT_TRUE(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.
|
|
EXPECT_TRUE(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);
|
|
EXPECT_TRUE(db != NULL);
|
|
EXPECT_TRUE(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);
|
|
EXPECT_TRUE(id != NULL);
|
|
EXPECT_TRUE(id[0] == '\0');
|
|
EXPECT_TRUE(oakengine_traverse_db_row_count(db, 0) >= 1);
|
|
|
|
// set_value_hint: unknown input ids and bogus types are rejected.
|
|
EXPECT_TRUE(oakengine_node_set_value_hint(solid, "not_an_input", -1,
|
|
OAK_NODE_VALUE_COLOR, 0,
|
|
NULL) == OAKENGINE_E_NOT_FOUND);
|
|
EXPECT_TRUE(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.
|
|
EXPECT_TRUE(oakengine_node_set_value_hint(lut, "tex_in", -1,
|
|
OAK_NODE_VALUE_COLOR, -1,
|
|
NULL) == OAKENGINE_OK);
|
|
EXPECT_TRUE(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.
|
|
EXPECT_TRUE(oakengine_node_set_value_hint(lut, "tex_in", -1,
|
|
OAK_NODE_VALUE_NONE, -1,
|
|
NULL) == OAKENGINE_OK);
|
|
EXPECT_TRUE(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.
|
|
EXPECT_TRUE(oakengine_traverse_transform(solid, solid, 0, 1, 1, 1, NULL, m) ==
|
|
OAKENGINE_OK);
|
|
EXPECT_TRUE(m[0] == 1.0 && m[1] == 0.0 && m[2] == 0.0 && m[3] == 1.0);
|
|
EXPECT_TRUE(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));
|
|
EXPECT_TRUE(oakengine_video_params_make(&vp, 1920, 1080, 1001, 30000, 0, 1, 1,
|
|
0, 0, 1) == OAKENGINE_OK);
|
|
memset(m, 0, sizeof(m));
|
|
EXPECT_TRUE(oakengine_traverse_transform(solid, lut, 0, 1, 1, 1, &vp, m) ==
|
|
OAKENGINE_OK);
|
|
EXPECT_TRUE(m[0] == 1.0 && m[1] == 0.0 && m[2] == 0.0 && m[3] == 1.0);
|
|
EXPECT_TRUE(m[4] == 0.0 && m[5] == 0.0);
|
|
}
|
|
|
|
TEST(OakEngineTraverse, Main)
|
|
{
|
|
make_tmpdir();
|
|
|
|
// Sandbox the config/cache/data locations.
|
|
#if !defined(_WIN32)
|
|
EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0);
|
|
EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0);
|
|
EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0);
|
|
EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0);
|
|
#endif
|
|
|
|
EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
|
|
|
OakEngineProject *project = oakengine_project_create();
|
|
EXPECT_TRUE(project != NULL);
|
|
EXPECT_TRUE(oakengine_project_new(project) == OAKENGINE_OK);
|
|
|
|
OakEngineNode *solid = oakengine_project_add_node(
|
|
project, "org.olivevideoeditor.Olive.solidgenerator");
|
|
EXPECT_TRUE(solid != NULL);
|
|
OakEngineNode *lut = oakengine_project_add_node(
|
|
project, "org.olivevideoeditor.Olive.ociolut");
|
|
EXPECT_TRUE(lut != NULL);
|
|
|
|
test_null_robustness(solid);
|
|
test_database(solid);
|
|
test_table_and_hints(solid, lut);
|
|
test_transform(solid, lut);
|
|
|
|
oakengine_project_free(project);
|
|
EXPECT_TRUE(oakengine_shutdown() == OAKENGINE_OK);
|
|
|
|
}
|