Files
oak-editor/crates/oaknode/tests/ffi_contract_test.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

1125 lines
35 KiB
Rust

// Oak Video Editor - 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/>.
//! C ABI contract tests (ffi.rs). One normal + one error path per
//! export family; the exhaustive matrix is driven from the existing
//! C++ gtest suite (`src/node/tests`, unchanged) running against this
//! crate — these tests only pin Rust-side specifics.
//!
//! Handles follow the C by-value contract: every function call that
//! consumes a handle receives `dup(&h)` (addref + copy), and every dup
//! is eventually released. Tests serialize on [`ffi_lock`] because they
//! mutate the process-wide debug alive counter.
use std::ffi::{c_char, CString};
use std::sync::{Mutex, MutexGuard};
use oaknode::error::{OAKNODE_E_INVALID, OAKNODE_E_NOT_FOUND, OAKNODE_OK};
use oaknode::ffi::factory::{
oaknode_factory_create_from_id, oaknode_factory_id_at, oaknode_factory_id_count,
oaknode_factory_initialize, oaknode_factory_name_from_id, oaknode_factory_node_at,
};
use oaknode::ffi::keyframe::oaknode_keyframe_opposing_bezier_type;
use oaknode::ffi::node::{
oaknode_debug_alive_count, oaknode_node_are_linked, oaknode_node_connect,
oaknode_node_context_count, oaknode_node_context_node_at, oaknode_node_copy_inputs,
oaknode_node_create_copy, oaknode_node_disconnect, oaknode_node_free,
oaknode_node_from_identity, oaknode_node_get_context_position, oaknode_node_get_id,
oaknode_node_get_input, oaknode_node_get_input_at_time, oaknode_node_get_input_name,
oaknode_node_get_input_string, oaknode_node_get_label, oaknode_node_get_name,
oaknode_node_get_override_color, oaknode_node_get_project, oaknode_node_identity,
oaknode_node_input_array_insert, oaknode_node_input_array_remove, oaknode_node_input_count,
oaknode_node_input_get_connected_node, oaknode_node_input_get_type, oaknode_node_input_id,
oaknode_node_input_is_connectable, oaknode_node_input_is_connected, oaknode_node_is_enabled,
oaknode_node_link, oaknode_node_link_at, oaknode_node_link_count,
oaknode_node_output_connection_count, oaknode_node_output_connection_element_at,
oaknode_node_output_connection_input_id_at, oaknode_node_output_connection_node_at,
oaknode_node_remove_from_context, oaknode_node_set_context_position, oaknode_node_set_enabled,
oaknode_node_set_input, oaknode_node_set_input_string, oaknode_node_set_label,
oaknode_node_set_override_color, oaknode_node_set_value_hint_track, oaknode_node_unlink,
};
use oaknode::ffi::project::{
oaknode_project_add_node, oaknode_project_cache_path, oaknode_project_clear,
oaknode_project_copy_settings, oaknode_project_filename, oaknode_project_free,
oaknode_project_get_cache_location_setting, oaknode_project_get_custom_cache_path,
oaknode_project_get_uuid, oaknode_project_init, oaknode_project_initialize,
oaknode_project_is_modified, oaknode_project_is_new, oaknode_project_name,
oaknode_project_node_at, oaknode_project_node_count, oaknode_project_pretty_filename,
oaknode_project_remove_node, oaknode_project_root, oaknode_project_set_cache_location_setting,
oaknode_project_set_custom_cache_path, oaknode_project_set_filename,
oaknode_project_set_modified,
};
use oaknode::handle::CHandle;
use oaknode::value::{oak, OakNodeValue};
/// Serialize all tests touching the global alive counter / factory.
static FFI_LOCK: Mutex<()> = Mutex::new(());
fn ffi_lock() -> MutexGuard<'static, ()> {
FFI_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
/// By-value handle copy. The ffi callees never release the handles they
/// receive, so a bitwise clone is safe here: every box is released
/// exactly once, via the original handle's free call.
fn dup(h: &CHandle) -> CHandle {
h.clone()
}
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// Read a two-stage getter's result into a String; the caller passes
/// the getter closure (returns required size / error code).
fn two_stage<F: Fn(*mut c_char, i32) -> i32>(getter: F) -> Result<String, i32> {
let needed = getter(std::ptr::null_mut(), 0);
if needed < 0 {
return Err(needed);
}
let mut buf = vec![0u8; needed as usize];
let rc = getter(buf.as_mut_ptr() as *mut c_char, needed);
if rc < 0 {
return Err(rc);
}
buf.pop(); // trailing NUL
Ok(String::from_utf8(buf).unwrap())
}
fn init_project() -> CHandle {
let p = unsafe { oaknode_project_init() };
assert!(!p.ctx.is_null());
assert_eq!(p.abi_version, 1);
assert!(p.addref.is_some() && p.release.is_some());
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
p
}
/// Every implemented handle-returning function returns ctx==NULL on
/// failure and a valid refcounted handle on success (abi_version
/// stamped).
#[test]
fn handle_contract_all_exports() {
let _g = ffi_lock();
let mut p = init_project();
// Borrowed root folder handle.
let mut root = unsafe { oaknode_project_root(dup(&p)) };
assert!(!root.ctx.is_null());
assert_eq!(root.abi_version, 1);
// node_at: valid index -> borrowed handle; out-of-range / negative ->
// empty; empty project handle -> empty.
let mut n0 = unsafe { oaknode_project_node_at(dup(&p), 0) };
assert!(!n0.ctx.is_null());
assert!(unsafe { oaknode_project_node_at(dup(&p), 1) }.ctx.is_null());
assert!(unsafe { oaknode_project_node_at(dup(&p), -1) }
.ctx
.is_null());
assert!(unsafe { oaknode_project_node_at(CHandle::null(), 0) }
.ctx
.is_null());
// Factory node handle: owned, valid.
let mut math =
unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) };
assert!(!math.ctx.is_null());
assert_eq!(math.abi_version, 1);
// create_copy: owned copy of an orphan node.
let mut copy = unsafe { oaknode_node_create_copy(dup(&math)) };
assert!(!copy.ctx.is_null());
// from_identity of a live node -> valid; garbage identity -> empty.
let id = unsafe { oaknode_node_identity(dup(&math)) };
let mut back = unsafe { oaknode_node_from_identity(id) };
assert!(!back.ctx.is_null());
assert!(unsafe { oaknode_node_from_identity(0xdead) }.ctx.is_null());
unsafe { oaknode_node_free(&mut back) };
unsafe { oaknode_node_free(&mut copy) };
unsafe { oaknode_node_free(&mut math) };
unsafe { oaknode_node_free(&mut n0) };
unsafe { oaknode_node_free(&mut root) };
unsafe { oaknode_project_free(&mut p) };
}
/// free(NULL)/free(empty) are no-ops across every free export.
#[test]
fn free_null_noop_all_exports() {
let _g = ffi_lock();
unsafe {
oaknode_project_free(std::ptr::null_mut());
let mut empty = CHandle::null();
oaknode_project_free(&mut empty);
assert!(empty.ctx.is_null());
oaknode_node_free(std::ptr::null_mut());
let mut empty = CHandle::null();
oaknode_node_free(&mut empty);
assert!(empty.ctx.is_null());
}
// A live project frees cleanly and clears the handle.
let mut p = init_project();
unsafe { oaknode_project_free(&mut p) };
assert!(p.ctx.is_null());
}
/// Two-stage string functions: size query, short buffer truncation
/// rule, and exact-fit write — for every string getter.
#[test]
fn two_stage_string_contract() {
let _g = ffi_lock();
let mut p = init_project();
// project_name: "(untitled)" -> 10 + 1 bytes.
let name = two_stage(|buf, size| unsafe { oaknode_project_name(dup(&p), buf, size) }).unwrap();
assert_eq!(name, "(untitled)");
// Short buffer truncates and NUL-terminates.
let mut buf = [0u8; 4];
let rc =
unsafe { oaknode_project_name(dup(&p), buf.as_mut_ptr() as *mut c_char, buf.len() as i32) };
assert_eq!(
rc,
"(untitled)".len() as i32 + 1,
"required size regardless of buffer"
);
assert_eq!(&buf[..3], b"(un");
assert_eq!(buf[3], 0);
// Exact-fit write (required size) is NUL-terminated.
let needed = unsafe { oaknode_project_name(dup(&p), std::ptr::null_mut(), 0) };
let mut buf = vec![0u8; needed as usize];
assert_eq!(
unsafe { oaknode_project_name(dup(&p), buf.as_mut_ptr() as *mut c_char, needed) },
needed
);
assert_eq!(buf[needed as usize - 1], 0, "terminating NUL");
buf.pop();
assert_eq!(String::from_utf8(buf).unwrap(), "(untitled)");
// filename: "" initially (size 1); after set_filename -> full path.
let filename =
two_stage(|buf, size| unsafe { oaknode_project_filename(dup(&p), buf, size) }).unwrap();
assert_eq!(filename, "");
unsafe { oaknode_project_set_filename(dup(&p), cs("/tmp/demo.ove").as_ptr()) };
let filename =
two_stage(|buf, size| unsafe { oaknode_project_filename(dup(&p), buf, size) }).unwrap();
assert_eq!(filename, "/tmp/demo.ove");
let pretty =
two_stage(|buf, size| unsafe { oaknode_project_pretty_filename(dup(&p), buf, size) })
.unwrap();
assert_eq!(pretty, "/tmp/demo.ove");
// uuid is the 36-char braced format.
let uuid =
two_stage(|buf, size| unsafe { oaknode_project_get_uuid(dup(&p), buf, size) }).unwrap();
assert_eq!(uuid.len(), 38);
assert!(uuid.starts_with('{') && uuid.ends_with('}'));
// cache_path is a two-stage getter too.
let _ =
two_stage(|buf, size| unsafe { oaknode_project_cache_path(dup(&p), buf, size) }).unwrap();
// Node string getters.
let mut math =
unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) };
let id = two_stage(|buf, size| unsafe { oaknode_node_get_id(dup(&math), buf, size) }).unwrap();
assert_eq!(id, "org.olivevideoeditor.Olive.math");
let name =
two_stage(|buf, size| unsafe { oaknode_node_get_name(dup(&math), buf, size) }).unwrap();
assert_eq!(name, "Math");
let label =
two_stage(|buf, size| unsafe { oaknode_node_get_label(dup(&math), buf, size) }).unwrap();
assert_eq!(label, "");
let iname = two_stage(|buf, size| unsafe {
oaknode_node_get_input_name(dup(&math), cs("enabled_in").as_ptr(), buf, size)
})
.unwrap();
assert_eq!(iname, "Enabled");
unsafe { oaknode_node_free(&mut math) };
unsafe { oaknode_project_free(&mut p) };
}
/// Identity registry: node_identity / node_from_identity round-trip;
/// freed nodes are rejected by from_identity.
#[test]
fn identity_registry_roundtrip() {
let _g = ffi_lock();
let mut math =
unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) };
let id = unsafe { oaknode_node_identity(dup(&math)) };
// Round-trip: the looked-up node has the same type id.
let mut back = unsafe { oaknode_node_from_identity(id) };
assert!(!back.ctx.is_null());
let bid = two_stage(|buf, size| unsafe { oaknode_node_get_id(dup(&back), buf, size) }).unwrap();
assert_eq!(bid, "org.olivevideoeditor.Olive.math");
unsafe { oaknode_node_free(&mut back) };
// Move the node into a project: its identity is re-registered and
// still resolvable while the project lives.
let mut p = unsafe { oaknode_project_init() };
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&math)) },
OAKNODE_OK
);
let id2 = unsafe { oaknode_node_identity(dup(&math)) };
let mut back2 = unsafe { oaknode_node_from_identity(id2) };
assert!(!back2.ctx.is_null());
unsafe { oaknode_node_free(&mut back2) };
// Free the node handle (drops its project reference) and the project:
// both identities must then reject.
unsafe { oaknode_node_free(&mut math) };
unsafe { oaknode_project_free(&mut p) };
let ghost = unsafe { oaknode_node_from_identity(id) };
assert!(
ghost.ctx.is_null(),
"freed node must be rejected by from_identity"
);
let ghost2 = unsafe { oaknode_node_from_identity(id2) };
assert!(ghost2.ctx.is_null());
}
/// alive count: project create/destroy moves oaknode_debug_alive_count
/// predictably and returns to baseline.
#[test]
fn alive_count_accounting() {
let _g = ffi_lock();
let base = unsafe { oaknode_debug_alive_count() };
let mut p = init_project();
assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 1);
let mut math =
unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) };
assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 2);
// A borrowed view does not count.
let mut n0 = unsafe { oaknode_project_node_at(dup(&p), 0) };
assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 2);
unsafe { oaknode_node_free(&mut n0) };
// Adding the owned node to the project transfers its accounting to
// the project.
unsafe { oaknode_project_add_node(dup(&p), dup(&math)) };
assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 1);
// The node handle is now a borrowed view: freeing it changes nothing.
unsafe { oaknode_node_free(&mut math) };
assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 1);
unsafe { oaknode_project_free(&mut p) };
assert_eq!(unsafe { oaknode_debug_alive_count() }, base);
// Detach: remove_node returns ownership (counted again), free
// returns to baseline.
let mut p2 = init_project();
let mut node =
unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) };
unsafe { oaknode_project_add_node(dup(&p2), dup(&node)) };
assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 1);
unsafe { oaknode_project_remove_node(dup(&p2), dup(&node)) };
assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 2);
unsafe { oaknode_node_free(&mut node) };
assert_eq!(unsafe { oaknode_debug_alive_count() }, base + 1);
unsafe { oaknode_project_free(&mut p2) };
assert_eq!(unsafe { oaknode_debug_alive_count() }, base);
}
/// Project family: success + failure path for every implemented export.
#[test]
fn project_family_contract() {
let _g = ffi_lock();
let mut p = init_project();
// initialize: E_STATE on second call; E_INVALID on empty handle.
assert_eq!(
unsafe { oaknode_project_initialize(dup(&p)) },
oaknode::error::OAKNODE_E_STATE
);
assert_eq!(
unsafe { oaknode_project_initialize(CHandle::null()) },
OAKNODE_E_INVALID
);
// root: valid after initialize.
let mut root = unsafe { oaknode_project_root(dup(&p)) };
assert!(!root.ctx.is_null());
assert!(unsafe { oaknode_project_root(CHandle::null()) }
.ctx
.is_null());
unsafe { oaknode_node_free(&mut root) };
// name/filename/pretty/is_modified/is_new + empty-handle failures.
assert_eq!(
unsafe { oaknode_project_name(CHandle::null(), std::ptr::null_mut(), 0) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_filename(CHandle::null(), std::ptr::null_mut(), 0) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_is_modified(CHandle::null()) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_is_new(CHandle::null()) },
OAKNODE_E_INVALID
);
// set_filename: success; NULL failure.
assert_eq!(
unsafe { oaknode_project_set_filename(dup(&p), cs("/tmp/x.ove").as_ptr()) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_project_set_filename(dup(&p), std::ptr::null()) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_set_filename(CHandle::null(), cs("x").as_ptr()) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_is_new(dup(&p)) },
0,
"has a filename now"
);
// modified flag.
assert_eq!(
unsafe { oaknode_project_set_modified(dup(&p), 1) },
OAKNODE_OK
);
assert_eq!(unsafe { oaknode_project_is_modified(dup(&p)) }, 1);
assert_eq!(
unsafe { oaknode_project_set_modified(CHandle::null(), 0) },
OAKNODE_E_INVALID
);
// cache settings.
assert_eq!(
unsafe { oaknode_project_get_cache_location_setting(dup(&p)) },
0
);
assert_eq!(
unsafe { oaknode_project_set_cache_location_setting(dup(&p), 2) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_project_set_cache_location_setting(dup(&p), 99) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_set_cache_location_setting(CHandle::null(), 0) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_set_custom_cache_path(dup(&p), cs("/tmp/cache").as_ptr()) },
OAKNODE_OK
);
let custom =
two_stage(|buf, size| unsafe { oaknode_project_get_custom_cache_path(dup(&p), buf, size) })
.unwrap();
assert_eq!(custom, "/tmp/cache");
unsafe { oaknode_project_set_custom_cache_path(dup(&p), std::ptr::null()) }; // NULL clears
let custom =
two_stage(|buf, size| unsafe { oaknode_project_get_custom_cache_path(dup(&p), buf, size) })
.unwrap();
assert_eq!(custom, "");
// copy_settings: dst inherits src settings; empty handle fails.
let mut p2 = unsafe { oaknode_project_init() };
unsafe { oaknode_project_set_cache_location_setting(dup(&p2), 1) };
assert_eq!(
unsafe { oaknode_project_copy_settings(dup(&p), dup(&p2)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_project_get_cache_location_setting(dup(&p)) },
1
);
assert_eq!(
unsafe { oaknode_project_copy_settings(CHandle::null(), dup(&p2)) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_copy_settings(dup(&p), CHandle::null()) },
OAKNODE_E_INVALID
);
// node add/remove/count/at.
let base_count = unsafe { oaknode_project_node_count(dup(&p)) };
let mut node = unsafe {
oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.solidgenerator").as_ptr())
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&node)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_project_node_count(dup(&p)) },
base_count + 1
);
assert_eq!(
unsafe { oaknode_project_add_node(CHandle::null(), dup(&node)) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), CHandle::null()) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_remove_node(dup(&p), dup(&node)) },
OAKNODE_OK
);
assert_eq!(unsafe { oaknode_project_node_count(dup(&p)) }, base_count);
assert_eq!(
unsafe { oaknode_project_remove_node(dup(&p), dup(&node)) },
OAKNODE_E_NOT_FOUND,
"double remove fails"
);
assert_eq!(
unsafe { oaknode_project_remove_node(CHandle::null(), dup(&node)) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_project_node_count(CHandle::null()) },
OAKNODE_E_INVALID
);
unsafe { oaknode_node_free(&mut node) };
// clear: resets; empty handle fails.
assert_eq!(
unsafe { oaknode_project_clear(CHandle::null()) },
OAKNODE_E_INVALID
);
assert_eq!(unsafe { oaknode_project_clear(dup(&p)) }, OAKNODE_OK);
assert!(
unsafe { oaknode_project_root(dup(&p)) }.ctx.is_null(),
"root gone after clear"
);
assert_eq!(
unsafe { oaknode_project_initialize(dup(&p)) },
OAKNODE_OK,
"re-initializable"
);
unsafe { oaknode_project_free(&mut p) };
unsafe { oaknode_project_free(&mut p2) };
}
/// Node family: metadata, inputs, params, graph editing, links,
/// contexts, arrays, copies — success + failure per export.
#[test]
fn node_family_contract() {
let _g = ffi_lock();
let mut p = init_project();
// Two factory nodes in the same project.
let mut a =
unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.math").as_ptr()) };
let mut b =
unsafe { oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.ortho").as_ptr()) };
unsafe { oaknode_project_add_node(dup(&p), dup(&a)) };
unsafe { oaknode_project_add_node(dup(&p), dup(&b)) };
// Metadata.
assert_eq!(
unsafe { oaknode_node_get_id(CHandle::null(), std::ptr::null_mut(), 0) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_node_get_name(CHandle::null(), std::ptr::null_mut(), 0) },
OAKNODE_E_INVALID
);
let id = two_stage(|buf, size| unsafe { oaknode_node_get_id(dup(&a), buf, size) }).unwrap();
assert_eq!(id, "org.olivevideoeditor.Olive.math");
// Label.
assert_eq!(
unsafe { oaknode_node_set_label(dup(&a), cs("my node").as_ptr()) },
OAKNODE_OK
);
let label =
two_stage(|buf, size| unsafe { oaknode_node_get_label(dup(&a), buf, size) }).unwrap();
assert_eq!(label, "my node");
assert_eq!(
unsafe { oaknode_node_set_label(CHandle::null(), cs("x").as_ptr()) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_node_set_label(dup(&a), std::ptr::null()) },
OAKNODE_E_INVALID
);
// Override color.
let mut oc = 0;
assert_eq!(
unsafe { oaknode_node_get_override_color(dup(&a), &mut oc) },
OAKNODE_OK
);
assert_eq!(oc, -1);
assert_eq!(
unsafe { oaknode_node_set_override_color(dup(&a), 3) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_node_get_override_color(dup(&a), &mut oc) },
OAKNODE_OK
);
assert_eq!(oc, 3);
assert_eq!(
unsafe { oaknode_node_get_override_color(CHandle::null(), &mut oc) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_node_get_override_color(dup(&a), std::ptr::null_mut()) },
OAKNODE_E_INVALID
);
// Enabled.
let mut en = 0;
assert_eq!(
unsafe { oaknode_node_is_enabled(dup(&a), &mut en) },
OAKNODE_OK
);
assert_eq!(en, 1);
assert_eq!(unsafe { oaknode_node_set_enabled(dup(&a), 0) }, OAKNODE_OK);
assert_eq!(
unsafe { oaknode_node_is_enabled(dup(&a), &mut en) },
OAKNODE_OK
);
assert_eq!(en, 0);
assert_eq!(
unsafe { oaknode_node_is_enabled(CHandle::null(), &mut en) },
OAKNODE_E_INVALID
);
// Input introspection.
let mut count = 0;
assert_eq!(
unsafe { oaknode_node_input_count(dup(&a), &mut count) },
OAKNODE_OK
);
assert_eq!(count, 4, "enabled_in + method_in + param_a_in + param_b_in");
let first =
two_stage(|buf, size| unsafe { oaknode_node_input_id(dup(&a), 0, buf, size) }).unwrap();
assert_eq!(first, "enabled_in");
assert_eq!(
unsafe { oaknode_node_input_id(dup(&a), 99, std::ptr::null_mut(), 0) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe { oaknode_node_input_id(dup(&a), -1, std::ptr::null_mut(), 0) },
OAKNODE_E_NOT_FOUND
);
let mut ty = 0;
assert_eq!(
unsafe { oaknode_node_input_get_type(dup(&a), cs("enabled_in").as_ptr(), &mut ty) },
OAKNODE_OK
);
assert_eq!(ty, oak::BOOL);
assert_eq!(
unsafe { oaknode_node_input_get_type(dup(&a), cs("nope").as_ptr(), &mut ty) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe {
oaknode_node_input_get_type(dup(&a), cs("enabled_in").as_ptr(), std::ptr::null_mut())
},
OAKNODE_E_INVALID
);
let mut conn = 0;
assert_eq!(
unsafe { oaknode_node_input_is_connected(dup(&a), cs("param_a_in").as_ptr(), &mut conn) },
OAKNODE_OK
);
assert_eq!(conn, 0);
assert_eq!(
unsafe { oaknode_node_input_is_connected(dup(&a), cs("nope").as_ptr(), &mut conn) },
OAKNODE_E_NOT_FOUND
);
let mut connectable = 0;
assert_eq!(
unsafe {
oaknode_node_input_is_connectable(dup(&a), cs("param_a_in").as_ptr(), &mut connectable)
},
OAKNODE_OK
);
assert_eq!(connectable, 1);
assert_eq!(
unsafe {
oaknode_node_input_is_connectable(dup(&a), cs("method_in").as_ptr(), &mut connectable)
},
OAKNODE_OK
);
assert_eq!(connectable, 0, "method_in is not connectable");
assert_eq!(
unsafe {
oaknode_node_input_is_connectable(dup(&a), cs("nope").as_ptr(), &mut connectable)
},
OAKNODE_E_NOT_FOUND
);
let iname = two_stage(|buf, size| unsafe {
oaknode_node_get_input_name(dup(&a), cs("enabled_in").as_ptr(), buf, size)
})
.unwrap();
assert_eq!(iname, "Enabled");
assert_eq!(
unsafe {
oaknode_node_get_input_name(dup(&a), cs("nope").as_ptr(), std::ptr::null_mut(), 0)
},
OAKNODE_E_NOT_FOUND
);
// Params: get/set float, unknown id, string input, bad POD type.
let mut pod = OakNodeValue::none();
assert_eq!(
unsafe { oaknode_node_get_input(dup(&a), cs("param_a_in").as_ptr(), &mut pod) },
OAKNODE_OK
);
assert_eq!(pod.kind, oak::FLOAT);
assert_eq!(pod.f[0], 0.0);
let v = OakNodeValue {
kind: oak::FLOAT,
num: 0,
den: 0,
f: [3.5, 0.0, 0.0, 0.0],
};
assert_eq!(
unsafe { oaknode_node_set_input(dup(&a), cs("param_a_in").as_ptr(), &v) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_node_get_input(dup(&a), cs("param_a_in").as_ptr(), &mut pod) },
OAKNODE_OK
);
assert_eq!(pod.f[0], 3.5);
assert_eq!(
unsafe { oaknode_node_get_input(dup(&a), cs("nope").as_ptr(), &mut pod) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe { oaknode_node_set_input(dup(&a), cs("nope").as_ptr(), &v) },
OAKNODE_E_NOT_FOUND
);
let wrong_type = OakNodeValue {
kind: oak::INT,
num: 1,
den: 0,
f: [0.0; 4],
};
assert_eq!(
unsafe { oaknode_node_set_input(dup(&a), cs("param_a_in").as_ptr(), &wrong_type) },
OAKNODE_E_INVALID,
"POD type must match the declared input type"
);
// String input: the timeformat node's format_in.
let mut tf = unsafe {
oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.timeformat").as_ptr())
};
unsafe { oaknode_project_add_node(dup(&p), dup(&tf)) };
let s = two_stage(|buf, size| unsafe {
oaknode_node_get_input_string(dup(&tf), cs("format_in").as_ptr(), buf, size)
})
.unwrap();
assert_eq!(s, "hh:mm:ss");
assert_eq!(
unsafe {
oaknode_node_set_input_string(dup(&tf), cs("format_in").as_ptr(), cs("yyyy").as_ptr())
},
OAKNODE_OK
);
let s = two_stage(|buf, size| unsafe {
oaknode_node_get_input_string(dup(&tf), cs("format_in").as_ptr(), buf, size)
})
.unwrap();
assert_eq!(s, "yyyy");
// Non-string input -> E_INVALID; unknown -> E_NOT_FOUND.
assert_eq!(
unsafe {
oaknode_node_get_input_string(dup(&tf), cs("time_in").as_ptr(), std::ptr::null_mut(), 0)
},
OAKNODE_E_INVALID
);
assert_eq!(
unsafe {
oaknode_node_set_input_string(dup(&tf), cs("time_in").as_ptr(), cs("x").as_ptr())
},
OAKNODE_E_INVALID
);
assert_eq!(
unsafe {
oaknode_node_get_input_string(dup(&tf), cs("nope").as_ptr(), std::ptr::null_mut(), 0)
},
OAKNODE_E_NOT_FOUND
);
// Graph editing: connect a -> b.param_a_in.
assert_eq!(
unsafe { oaknode_node_connect(dup(&a), dup(&b), cs("rot_in").as_ptr()) },
OAKNODE_OK
);
let mut connected = 0;
assert_eq!(
unsafe { oaknode_node_input_is_connected(dup(&b), cs("rot_in").as_ptr(), &mut connected) },
OAKNODE_OK
);
assert_eq!(connected, 1);
// Duplicate connect -> E_STATE.
assert_eq!(
unsafe { oaknode_node_connect(dup(&a), dup(&b), cs("rot_in").as_ptr()) },
oaknode::error::OAKNODE_E_STATE
);
// Not connectable -> E_INVALID.
assert_eq!(
unsafe { oaknode_node_connect(dup(&a), dup(&a), cs("method_in").as_ptr()) },
OAKNODE_E_INVALID
);
// Unknown input -> E_NOT_FOUND.
assert_eq!(
unsafe { oaknode_node_connect(dup(&a), dup(&b), cs("nope").as_ptr()) },
OAKNODE_E_NOT_FOUND
);
// Empty handle -> E_INVALID.
assert_eq!(
unsafe { oaknode_node_connect(CHandle::null(), dup(&b), cs("param_a_in").as_ptr()) },
OAKNODE_E_INVALID
);
// input_get_connected_node.
let mut src = CHandle::null();
assert_eq!(
unsafe { oaknode_node_input_get_connected_node(dup(&b), cs("rot_in").as_ptr(), &mut src) },
OAKNODE_OK
);
assert!(!src.ctx.is_null());
let src_id =
two_stage(|buf, size| unsafe { oaknode_node_get_id(dup(&src), buf, size) }).unwrap();
assert_eq!(src_id, "org.olivevideoeditor.Olive.math");
assert_eq!(
unsafe { oaknode_node_input_get_connected_node(dup(&b), cs("nope").as_ptr(), &mut src) },
OAKNODE_E_NOT_FOUND
);
// Output connections.
let mut count = 0;
assert_eq!(
unsafe { oaknode_node_output_connection_count(dup(&a), &mut count) },
OAKNODE_OK
);
assert_eq!(count, 1);
let mut node_out = CHandle::null();
assert_eq!(
unsafe { oaknode_node_output_connection_node_at(dup(&a), 0, &mut node_out) },
OAKNODE_OK
);
assert!(!node_out.ctx.is_null());
assert_eq!(
unsafe { oaknode_node_output_connection_node_at(dup(&a), 1, &mut node_out) },
OAKNODE_E_NOT_FOUND
);
let input_id = two_stage(|buf, size| unsafe {
oaknode_node_output_connection_input_id_at(dup(&a), 0, buf, size)
})
.unwrap();
assert_eq!(input_id, "rot_in");
let mut element = 0;
assert_eq!(
unsafe { oaknode_node_output_connection_element_at(dup(&a), 0, &mut element) },
OAKNODE_OK
);
assert_eq!(element, -1);
// Disconnect.
assert_eq!(
unsafe { oaknode_node_disconnect(dup(&b), cs("rot_in").as_ptr()) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_node_disconnect(dup(&b), cs("rot_in").as_ptr()) },
OAKNODE_E_NOT_FOUND,
"disconnect of an unconnected input fails"
);
assert_eq!(
unsafe { oaknode_node_disconnect(dup(&b), cs("nope").as_ptr()) },
OAKNODE_E_NOT_FOUND
);
// Links.
let mut linked = 0;
assert_eq!(
unsafe { oaknode_node_link(dup(&a), dup(&b), &mut linked) },
OAKNODE_OK
);
assert_eq!(linked, 1);
assert_eq!(
unsafe { oaknode_node_link(dup(&a), dup(&b), &mut linked) },
OAKNODE_OK
);
assert_eq!(linked, 0, "already linked");
let mut linked_ok = 0;
assert_eq!(
unsafe { oaknode_node_are_linked(dup(&a), dup(&b), &mut linked_ok) },
OAKNODE_OK
);
assert_eq!(linked_ok, 1);
assert_eq!(
unsafe { oaknode_node_link_count(dup(&a), &mut count) },
OAKNODE_OK
);
assert_eq!(count, 1);
let mut link_node = CHandle::null();
assert_eq!(
unsafe { oaknode_node_link_at(dup(&a), 0, &mut link_node) },
OAKNODE_OK
);
assert!(!link_node.ctx.is_null());
assert_eq!(
unsafe { oaknode_node_link_at(dup(&a), 1, &mut link_node) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe { oaknode_node_unlink(dup(&a), dup(&b), &mut linked) },
OAKNODE_OK
);
assert_eq!(linked, 1);
assert_eq!(
unsafe { oaknode_node_unlink(dup(&a), dup(&b), &mut linked) },
OAKNODE_OK
);
assert_eq!(linked, 0, "already unlinked");
// Context positions.
assert_eq!(
unsafe { oaknode_node_set_context_position(dup(&a), dup(&b), 10.0, 20.0, 1) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_node_context_count(dup(&a), &mut count) },
OAKNODE_OK
);
assert_eq!(count, 1);
let mut x = 0.0;
let mut y = 0.0;
let mut expanded = 0;
assert_eq!(
unsafe {
oaknode_node_get_context_position(dup(&a), dup(&b), &mut x, &mut y, &mut expanded)
},
OAKNODE_OK
);
assert_eq!((x, y, expanded), (10.0, 20.0, 1));
assert_eq!(
unsafe {
oaknode_node_get_context_position(dup(&a), dup(&a), &mut x, &mut y, &mut expanded)
},
OAKNODE_E_NOT_FOUND
);
let mut ctx_node = CHandle::null();
assert_eq!(
unsafe { oaknode_node_context_node_at(dup(&a), 0, &mut ctx_node) },
OAKNODE_OK
);
assert!(!ctx_node.ctx.is_null());
assert_eq!(
unsafe { oaknode_node_remove_from_context(dup(&a), dup(&b)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_node_remove_from_context(dup(&a), dup(&b)) },
OAKNODE_E_NOT_FOUND
);
// get_project.
let mut proj = CHandle::null();
assert_eq!(
unsafe { oaknode_node_get_project(dup(&a), &mut proj) },
OAKNODE_OK
);
assert!(!proj.ctx.is_null());
assert_eq!(
unsafe { oaknode_node_get_project(CHandle::null(), &mut proj) },
OAKNODE_E_INVALID
);
// Array input insert/remove on a non-array input fails E_INVALID;
// on an unknown input E_NOT_FOUND.
assert_eq!(
unsafe { oaknode_node_input_array_insert(dup(&a), cs("param_a_in").as_ptr(), 0) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_node_input_array_insert(dup(&a), cs("nope").as_ptr(), 0) },
OAKNODE_E_NOT_FOUND
);
// copy_inputs: dst (matrix) takes src (math)'s values where the ids
// match (enabled_in only) and preserves its own otherwise.
assert_eq!(
unsafe { oaknode_node_copy_inputs(dup(&b), dup(&a), 0) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_node_copy_inputs(CHandle::null(), dup(&a), 0) },
OAKNODE_E_INVALID
);
// set_value_hint_track.
assert_eq!(
unsafe { oaknode_node_set_value_hint_track(dup(&a), cs("param_a_in").as_ptr(), 0, 1) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_node_set_value_hint_track(dup(&a), cs("nope").as_ptr(), 0, 1) },
OAKNODE_E_NOT_FOUND
);
// get_input_at_time: no keyframes -> standard value.
let mut at = OakNodeValue::none();
assert_eq!(
unsafe {
oaknode_node_get_input_at_time(dup(&a), cs("param_a_in").as_ptr(), 5, 1, &mut at)
},
OAKNODE_OK
);
assert_eq!(at.kind, oak::FLOAT);
assert!((at.f[0] - 3.5).abs() < 1e-9);
assert_eq!(
unsafe { oaknode_node_get_input_at_time(dup(&a), cs("nope").as_ptr(), 5, 1, &mut at) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe {
oaknode_node_get_input_at_time(
dup(&a),
cs("param_a_in").as_ptr(),
5,
1,
std::ptr::null_mut(),
)
},
OAKNODE_E_INVALID
);
// create_copy of a graph-owned node.
let mut copy = unsafe { oaknode_node_create_copy(dup(&a)) };
assert!(!copy.ctx.is_null());
let copy_id =
two_stage(|buf, size| unsafe { oaknode_node_get_id(dup(&copy), buf, size) }).unwrap();
assert_eq!(copy_id, "org.olivevideoeditor.Olive.math");
// Cleanup (free every borrowed view and the owned nodes).
unsafe { oaknode_node_free(&mut copy) };
unsafe { oaknode_node_free(&mut src) };
unsafe { oaknode_node_free(&mut node_out) };
unsafe { oaknode_node_free(&mut link_node) };
unsafe { oaknode_node_free(&mut ctx_node) };
unsafe { oaknode_node_free(&mut proj) };
unsafe { oaknode_node_free(&mut tf) };
unsafe { oaknode_node_free(&mut b) };
unsafe { oaknode_node_free(&mut a) };
unsafe { oaknode_project_free(&mut p) };
}
/// Factory family + the pure keyframe helper.
#[test]
fn factory_and_keyframe_helpers() {
let _g = ffi_lock();
assert_eq!(unsafe { oaknode_factory_initialize() }, OAKNODE_OK);
assert_eq!(
unsafe { oaknode_factory_initialize() },
OAKNODE_OK,
"idempotent"
);
let mut count = 0;
assert_eq!(unsafe { oaknode_factory_id_count(&mut count) }, OAKNODE_OK);
assert!(count >= 4, "at least the implemented node types registered");
let first = two_stage(|buf, size| unsafe { oaknode_factory_id_at(0, buf, size) }).unwrap();
assert_eq!(first, "org.olivevideoeditor.Olive.polygon");
assert_eq!(
unsafe { oaknode_factory_id_at(-1, std::ptr::null_mut(), 0) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe { oaknode_factory_id_at(count, std::ptr::null_mut(), 0) },
OAKNODE_E_NOT_FOUND
);
let name = two_stage(|buf, size| unsafe {
oaknode_factory_name_from_id(cs("org.olivevideoeditor.Olive.pan").as_ptr(), buf, size)
})
.unwrap();
assert_eq!(name, "Pan");
let missing = two_stage(|buf, size| unsafe {
oaknode_factory_name_from_id(cs("org.example.missing").as_ptr(), buf, size)
})
.unwrap();
assert_eq!(missing, "", "unknown id -> empty string");
// create_from_id: known -> valid, unknown -> empty.
let mut node = unsafe {
oaknode_factory_create_from_id(cs("org.olivevideoeditor.Olive.solidgenerator").as_ptr())
};
assert!(!node.ctx.is_null());
assert!(
unsafe { oaknode_factory_create_from_id(cs("org.example.missing").as_ptr()) }
.ctx
.is_null()
);
assert!(unsafe { oaknode_factory_create_from_id(std::ptr::null()) }
.ctx
.is_null());
// node_at: borrowed prototype (index 5 = math, whose constructor is
// implemented; prototypes at todo!()-constructor indices are only
// reachable once Phase 3 lands); out-of-range -> E_NOT_FOUND.
let mut proto = CHandle::null();
assert_eq!(
unsafe { oaknode_factory_node_at(5, &mut proto) },
OAKNODE_OK
);
assert!(!proto.ctx.is_null());
assert_eq!(
unsafe { oaknode_factory_node_at(count, &mut proto) },
OAKNODE_E_NOT_FOUND
);
// Pure keyframe helper: IN(0) <-> OUT(1), invalid -> E_INVALID.
assert_eq!(unsafe { oaknode_keyframe_opposing_bezier_type(0) }, 1);
assert_eq!(unsafe { oaknode_keyframe_opposing_bezier_type(1) }, 0);
assert_eq!(
unsafe { oaknode_keyframe_opposing_bezier_type(2) },
OAKNODE_E_INVALID
);
unsafe { oaknode_node_free(&mut node) };
unsafe { oaknode_node_free(&mut proto) };
}