Files
oak-editor/crates/oaknode/tests/ffi_timeline_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

2585 lines
71 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/>.
//! Phase-2/3 ffi family contract tests: sequence, track, block, folder,
//! footage, colormanager, traverser, serializer, the undoable node
//! exports, the multicam grid family and the deferred bridge exports.
//! One success + one failure path per family.
use std::ffi::{c_char, c_int, CString};
use oaknode::error::{OAKNODE_E_INVALID, OAKNODE_E_NOT_FOUND, OAKNODE_E_STATE, OAKNODE_OK};
use oaknode::ffi::block::{
oaknode_block_as_node, oaknode_block_clip_create, oaknode_block_free, oaknode_block_from_node,
oaknode_block_gap_create, oaknode_block_get_enabled, oaknode_block_get_kind,
oaknode_block_get_length, oaknode_block_get_out, oaknode_block_set_enabled,
oaknode_block_set_out, oaknode_clip_get_media_in, oaknode_clip_get_speed,
oaknode_clip_set_media_in, oaknode_clip_set_speed, oaknode_transition_get_in_offset,
oaknode_transition_set_offsets_and_length,
};
use oaknode::ffi::colormanager::{
oaknode_colormanager_get_compliant_color_transform, oaknode_colormanager_get_config_filename,
oaknode_colormanager_get_display_count, oaknode_colormanager_get_native,
oaknode_colormanager_init, oaknode_colormanager_initialize,
oaknode_colormanager_set_config_filename,
};
use oaknode::ffi::dragger::{
oaknode_dragger_create, oaknode_dragger_drag, oaknode_dragger_end, oaknode_dragger_free,
oaknode_dragger_is_started, oaknode_dragger_start,
};
use oaknode::ffi::folder::{
oaknode_folder_add_child, oaknode_folder_as_node, oaknode_folder_child_at,
oaknode_folder_child_count, oaknode_folder_create, oaknode_folder_has_child_recursive,
oaknode_folder_index_of_child, oaknode_folder_parent_of, oaknode_folder_remove_child,
};
use oaknode::ffi::footage::{
oaknode_footage_as_node, oaknode_footage_clear_proxy, oaknode_footage_create,
oaknode_footage_filename, oaknode_footage_get_video_params, oaknode_footage_is_valid,
oaknode_footage_proxy_path, oaknode_footage_set_filename, oaknode_footage_set_proxy,
oaknode_footage_set_video_params,
};
use oaknode::ffi::group::{
oaknode_group_add_input_passthrough, oaknode_group_add_input_passthrough_undoable,
oaknode_group_cast, oaknode_group_create, oaknode_group_free,
oaknode_group_get_output_passthrough, oaknode_group_passthrough_count,
oaknode_group_passthrough_id_at, oaknode_group_passthrough_input_at,
oaknode_group_remove_input_passthrough, oaknode_group_resolve_input,
oaknode_group_set_output_passthrough, oaknode_group_set_output_passthrough_undoable,
};
use oaknode::ffi::keyframe::{
oaknode_keyframe_compute_paste_value, oaknode_keyframe_create, oaknode_keyframe_free,
oaknode_keyframe_get_bezier_control, oaknode_keyframe_get_element, oaknode_keyframe_get_input,
oaknode_keyframe_get_parent, oaknode_keyframe_get_time, oaknode_keyframe_get_track,
oaknode_keyframe_get_type, oaknode_keyframe_get_valid_bezier_control,
oaknode_keyframe_get_value, oaknode_keyframe_get_value_string,
oaknode_keyframe_has_sibling_at_time, oaknode_keyframe_opposing_bezier_type,
oaknode_keyframe_set_bezier_control, oaknode_keyframe_set_bezier_control_undoable,
oaknode_keyframe_set_time, oaknode_keyframe_set_time_undoable, oaknode_keyframe_set_type,
oaknode_keyframe_set_type_undoable, oaknode_keyframe_set_value,
oaknode_keyframe_set_value_string, oaknode_keyframe_set_value_string_undoable,
oaknode_keyframe_set_value_undoable,
};
use oaknode::ffi::multicam::{
oaknode_multicam_get_current_source, oaknode_multicam_get_rows_and_columns,
oaknode_multicam_get_source_count, oaknode_multicam_index_to_row_cols,
oaknode_multicam_input_current, oaknode_multicam_input_sequence,
oaknode_multicam_input_sequence_type, oaknode_multicam_input_sources,
oaknode_multicam_rows_cols_to_index,
};
use oaknode::ffi::node::{
oaknode_node_connect, oaknode_node_connect_undoable, oaknode_node_disconnect,
oaknode_node_disconnect_undoable, oaknode_node_find_input_footage, oaknode_node_get_input,
oaknode_node_get_input_at_time, oaknode_node_get_markers, oaknode_node_get_video_frame_cache,
oaknode_node_get_work_area, oaknode_node_set_enabled_undoable,
oaknode_node_set_input_at_time_undoable, oaknode_node_set_input_undoable,
oaknode_node_set_label_undoable, oaknode_viewer_set_audio_params,
oaknode_viewer_set_video_params,
};
use oaknode::ffi::project::{
oaknode_project_add_node, oaknode_project_free, oaknode_project_init,
oaknode_project_initialize, oaknode_project_node_at,
};
use oaknode::ffi::sequence::{
oaknode_sequence_as_node, oaknode_sequence_create, oaknode_sequence_free,
oaknode_sequence_from_node, oaknode_sequence_get_audio_params,
oaknode_sequence_get_audio_stream_count, oaknode_sequence_get_playhead,
oaknode_sequence_get_track_count, oaknode_sequence_get_track_list,
oaknode_sequence_get_video_params, oaknode_sequence_get_video_stream_count,
oaknode_sequence_set_audio_params, oaknode_sequence_set_default_parameters,
oaknode_sequence_set_playhead, oaknode_sequence_set_video_params,
oaknode_sequence_verify_length,
};
use oaknode::ffi::serializer::{
oaknode_serializer_initialize, oaknode_serializer_load_from_file,
oaknode_serializer_load_from_xml, oaknode_serializer_loaddata_free,
oaknode_serializer_loaddata_node_at, oaknode_serializer_loaddata_node_count,
oaknode_serializer_save_to_file, oaknode_serializer_save_to_xml,
oaknode_serializer_savedata_create, oaknode_serializer_savedata_free,
};
use oaknode::ffi::track::{
oaknode_track_append_block, oaknode_track_as_node, oaknode_track_create, oaknode_track_free,
oaknode_track_get_block_at, oaknode_track_get_block_count, oaknode_track_get_height,
oaknode_track_get_index, oaknode_track_get_length, oaknode_track_get_muted,
oaknode_track_get_reference, oaknode_track_get_type, oaknode_track_prepend_block,
oaknode_track_ripple_remove_block, oaknode_track_set_height, oaknode_track_set_index,
oaknode_track_set_muted, oaknode_tracklist_add_track, oaknode_tracklist_array_append,
oaknode_tracklist_get_array_size, oaknode_tracklist_get_track_at,
oaknode_tracklist_get_track_count, oaknode_tracklist_get_track_input_id,
oaknode_tracklist_get_type, oaknode_tracklist_remove_track,
};
use oaknode::ffi::traverser::{
oaknode_traverser_database_free, oaknode_traverser_database_row_count,
oaknode_traverser_generate_database, oaknode_traverser_init,
};
use oaknode::handle::CHandle;
use oaknode::value::{oak, OakNodeValue};
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn lock() -> std::sync::MutexGuard<'static, ()> {
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
fn dup(h: &CHandle) -> CHandle {
h.clone()
}
fn two_stage<F: FnMut(*mut c_char, i32) -> i32>(mut 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();
Ok(String::from_utf8(buf).unwrap())
}
fn free(h: &mut CHandle) {
if !h.ctx.is_null() {
unsafe { (h.release.unwrap())(h.ctx) };
h.ctx = std::ptr::null_mut();
}
}
/// Folder family: create/add/remove/query + failure paths.
#[test]
fn folder_family() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut folder = unsafe { oaknode_folder_create(dup(&p)) };
assert!(!folder.ctx.is_null());
assert!(unsafe { oaknode_folder_create(CHandle::null()) }
.ctx
.is_null());
let mut root = unsafe { oaknode_project_node_at(dup(&p), 0) };
let mut node = unsafe { oaknode_folder_as_node(dup(&folder)) };
assert!(!node.ctx.is_null());
// Add a subfolder as a child of the root, then re-parent it.
let mut child = unsafe { oaknode_folder_create(dup(&p)) };
assert_eq!(
unsafe { oaknode_folder_add_child(dup(&root), dup(&child)) },
OAKNODE_OK
);
assert_eq!(unsafe { oaknode_folder_child_count(dup(&root)) }, 1);
let mut child_at = unsafe { oaknode_folder_child_at(dup(&root), 0) };
assert!(!child_at.ctx.is_null());
assert!(unsafe { oaknode_folder_child_at(dup(&root), 5) }
.ctx
.is_null());
assert_eq!(
unsafe { oaknode_folder_index_of_child(dup(&root), dup(&child)) },
0
);
assert_eq!(
unsafe { oaknode_folder_index_of_child(dup(&root), dup(&folder)) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe { oaknode_folder_has_child_recursive(dup(&root), dup(&child)) },
1
);
let mut parent = unsafe { oaknode_folder_parent_of(dup(&child)) };
assert!(!parent.ctx.is_null());
assert!(unsafe { oaknode_folder_parent_of(dup(&folder)) }
.ctx
.is_null());
// Double-add to another folder fails E_STATE.
let mut other = unsafe { oaknode_folder_create(dup(&p)) };
assert_eq!(
unsafe { oaknode_folder_add_child(dup(&other), dup(&child)) },
OAKNODE_E_STATE
);
assert_eq!(
unsafe { oaknode_folder_remove_child(dup(&root), dup(&child)) },
OAKNODE_OK
);
assert_eq!(unsafe { oaknode_folder_child_count(dup(&root)) }, 0);
assert_eq!(
unsafe { oaknode_folder_remove_child(dup(&root), dup(&child)) },
OAKNODE_E_NOT_FOUND
);
free(&mut parent);
free(&mut child_at);
free(&mut child);
free(&mut other);
free(&mut node);
free(&mut folder);
free(&mut root);
unsafe { oaknode_project_free(&mut p) };
}
/// Sequence family: create → track lists, default params, playhead,
/// lengths + failure paths.
#[test]
fn sequence_family() {
let _g = lock();
let mut seq = unsafe { oaknode_sequence_create() };
assert!(!seq.ctx.is_null());
let mut node = unsafe { oaknode_sequence_as_node(dup(&seq)) };
assert!(!node.ctx.is_null());
let back = unsafe { oaknode_sequence_from_node(dup(&node)) };
assert!(!back.ctx.is_null());
let _ = back;
// Default parameters: one video + one audio stream.
assert_eq!(
unsafe { oaknode_sequence_set_default_parameters(dup(&seq)) },
OAKNODE_OK
);
let mut vc = 0;
assert_eq!(
unsafe { oaknode_sequence_get_audio_stream_count(dup(&seq), &mut vc) },
OAKNODE_OK
);
assert_eq!(vc, 1);
// Track lists exist with zero tracks.
let mut list = CHandle::null();
assert_eq!(
unsafe { oaknode_sequence_get_track_list(dup(&seq), 0, &mut list) },
OAKNODE_OK
);
assert!(!list.ctx.is_null());
assert_eq!(
unsafe { oaknode_sequence_get_track_list(dup(&seq), 99, &mut list) },
OAKNODE_E_NOT_FOUND
);
let mut tc = 0;
assert_eq!(
unsafe { oaknode_sequence_get_track_count(dup(&seq), 0, &mut tc) },
OAKNODE_OK
);
assert_eq!(tc, 0);
// Playhead round-trip.
assert_eq!(
unsafe { oaknode_sequence_set_playhead(dup(&seq), 30, 1) },
OAKNODE_OK
);
let mut num = 0;
let mut den = 0;
assert_eq!(
unsafe { oaknode_sequence_get_playhead(dup(&seq), &mut num, &mut den) },
OAKNODE_OK
);
assert_eq!((num, den), (30, 1));
// verify_length on an empty sequence.
assert_eq!(
unsafe { oaknode_sequence_verify_length(dup(&seq)) },
OAKNODE_OK
);
// Failure paths on empty handles.
assert_eq!(
unsafe { oaknode_sequence_get_playhead(CHandle::null(), &mut num, &mut den) },
OAKNODE_E_INVALID
);
assert!(unsafe { oaknode_sequence_from_node(CHandle::null()) }
.ctx
.is_null());
free(&mut list);
free(&mut node);
unsafe { oaknode_sequence_free(&mut seq) };
}
/// Track + tracklist family: block ordering primitives, track flags,
/// add/remove tracks + failure paths.
#[test]
fn track_family() {
let _g = lock();
let mut track = unsafe { oaknode_track_create(0) }; // video
assert!(!track.ctx.is_null());
assert!(unsafe { oaknode_track_create(99) }.ctx.is_null());
let mut ty = -1;
assert_eq!(
unsafe { oaknode_track_get_type(dup(&track), &mut ty) },
OAKNODE_OK
);
assert_eq!(ty, 0);
let mut node = unsafe { oaknode_track_as_node(dup(&track)) };
assert!(!node.ctx.is_null());
// Height (internal <-> pixels).
let mut h = 0.0;
assert_eq!(
unsafe { oaknode_track_get_height(dup(&track), &mut h) },
OAKNODE_OK
);
assert!((h - 3.0).abs() < 1e-9);
assert_eq!(
unsafe { oaknode_track_set_height(dup(&track), 5.0) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_track_get_height(dup(&track), &mut h) },
OAKNODE_OK
);
assert!((h - 5.0).abs() < 1e-9);
// Muted/locked/index.
assert_eq!(
unsafe { oaknode_track_set_muted(dup(&track), 1) },
OAKNODE_OK
);
let mut muted = 0;
assert_eq!(
unsafe { oaknode_track_get_muted(dup(&track), &mut muted) },
OAKNODE_OK
);
assert_eq!(muted, 1);
assert_eq!(
unsafe { oaknode_track_set_index(dup(&track), 2) },
OAKNODE_OK
);
let mut idx = 0;
assert_eq!(
unsafe { oaknode_track_get_index(dup(&track), &mut idx) },
OAKNODE_OK
);
assert_eq!(idx, 2);
let mut rty = 0;
let mut ridx = 0;
assert_eq!(
unsafe { oaknode_track_get_reference(dup(&track), &mut rty, &mut ridx) },
OAKNODE_OK
);
assert_eq!((rty, ridx), (0, 2));
// Blocks: append two clips, prepend one, count, remove.
let mut clip1 = unsafe { oaknode_block_clip_create() };
let mut clip2 = unsafe { oaknode_block_clip_create() };
let mut clip3 = unsafe { oaknode_block_clip_create() };
assert_eq!(
unsafe { oaknode_track_append_block(dup(&track), dup(&clip1)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_track_append_block(dup(&track), dup(&clip2)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_track_prepend_block(dup(&track), dup(&clip3)) },
OAKNODE_OK
);
let mut bc = 0;
assert_eq!(
unsafe { oaknode_track_get_block_count(dup(&track), &mut bc) },
OAKNODE_OK
);
assert_eq!(bc, 3);
let mut blk = CHandle::null();
assert_eq!(
unsafe { oaknode_track_get_block_at(dup(&track), 0, &mut blk) },
OAKNODE_OK
);
assert!(!blk.ctx.is_null());
assert_eq!(
unsafe { oaknode_track_get_block_at(dup(&track), 5, &mut blk) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe { oaknode_track_ripple_remove_block(dup(&track), dup(&clip1)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_track_get_block_count(dup(&track), &mut bc) },
OAKNODE_OK
);
assert_eq!(bc, 2);
// Track length via the blocks' ranges.
let mut len_num = 0;
let mut len_den = 0;
assert_eq!(
unsafe { oaknode_track_get_length(dup(&track), &mut len_num, &mut len_den) },
OAKNODE_OK
);
assert_eq!(len_num, 1, "blocks default to 1s length");
// Tracklist: build a sequence to host the list.
let mut seq = unsafe { oaknode_sequence_create() };
let mut snode = unsafe { oaknode_sequence_as_node(dup(&seq)) };
let mut list2 = CHandle::null();
assert_eq!(
unsafe { oaknode_sequence_get_track_list(dup(&seq), 0, &mut list2) },
OAKNODE_OK
);
let mut lt = 0;
assert_eq!(
unsafe { oaknode_tracklist_get_type(dup(&list2), &mut lt) },
OAKNODE_OK
);
assert_eq!(lt, 0);
let input_id =
two_stage(|b, s| unsafe { oaknode_tracklist_get_track_input_id(dup(&list2), b, s) })
.unwrap();
assert_eq!(input_id, "track_in_0");
assert_eq!(
unsafe { oaknode_tracklist_array_append(dup(&list2)) },
OAKNODE_OK
);
let mut asz = 0;
assert_eq!(
unsafe { oaknode_tracklist_get_array_size(dup(&list2), &mut asz) },
OAKNODE_OK
);
assert_eq!(asz, 1);
// Add the track to the list.
assert_eq!(
unsafe { oaknode_tracklist_add_track(dup(&list2), dup(&track)) },
OAKNODE_OK
);
let mut tcount = 0;
assert_eq!(
unsafe { oaknode_tracklist_get_track_count(dup(&list2), &mut tcount) },
OAKNODE_OK
);
assert_eq!(tcount, 1);
let mut track_at = CHandle::null();
assert_eq!(
unsafe { oaknode_tracklist_get_track_at(dup(&list2), 0, &mut track_at) },
OAKNODE_OK
);
assert!(!track_at.ctx.is_null());
assert_eq!(
unsafe { oaknode_tracklist_remove_track(dup(&list2), dup(&track)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_tracklist_remove_track(dup(&list2), dup(&track)) },
OAKNODE_E_NOT_FOUND
);
free(&mut track_at);
free(&mut list2);
free(&mut snode);
unsafe { oaknode_sequence_free(&mut seq) };
free(&mut blk);
free(&mut clip1);
free(&mut clip2);
free(&mut clip3);
free(&mut node);
unsafe { oaknode_track_free(&mut track) };
}
/// Block family: clip/gap creation, kind, in/out/length, media_in,
/// speed + failure paths.
#[test]
fn block_family() {
let _g = lock();
let mut clip = unsafe { oaknode_block_clip_create() };
let mut gap = unsafe { oaknode_block_gap_create() };
assert!(!clip.ctx.is_null());
assert!(!gap.ctx.is_null());
let mut kind = 0;
assert_eq!(
unsafe { oaknode_block_get_kind(dup(&clip), &mut kind) },
OAKNODE_OK
);
assert_eq!(kind, 1, "clip kind");
assert_eq!(
unsafe { oaknode_block_get_kind(dup(&gap), &mut kind) },
OAKNODE_OK
);
assert_eq!(kind, 2, "gap kind");
let mut node = unsafe { oaknode_block_as_node(dup(&clip)) };
assert!(!node.ctx.is_null());
let mut back = unsafe { oaknode_block_from_node(dup(&node)) };
assert!(!back.ctx.is_null());
assert!(unsafe { oaknode_block_from_node(CHandle::null()) }
.ctx
.is_null());
// Length / out round-trip.
let mut len_num = 0;
let mut len_den = 0;
assert_eq!(
unsafe { oaknode_block_get_length(dup(&clip), &mut len_num, &mut len_den) },
OAKNODE_OK
);
assert_eq!((len_num, len_den), (1, 1));
assert_eq!(
unsafe { oaknode_block_set_out(dup(&clip), 5, 1) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_block_get_out(dup(&clip), &mut len_num, &mut len_den) },
OAKNODE_OK
);
assert_eq!((len_num, len_den), (5, 1));
// Enabled.
let mut en = 0;
assert_eq!(
unsafe { oaknode_block_get_enabled(dup(&clip), &mut en) },
OAKNODE_OK
);
assert_eq!(en, 1);
assert_eq!(
unsafe { oaknode_block_set_enabled(dup(&clip), 0) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_block_get_enabled(dup(&clip), &mut en) },
OAKNODE_OK
);
assert_eq!(en, 0);
// Clip media_in/speed.
let mut min = 0;
let mut mden = 0;
assert_eq!(
unsafe { oaknode_clip_get_media_in(dup(&clip), &mut min, &mut mden) },
OAKNODE_OK
);
assert_eq!((min, mden), (0, 1));
assert_eq!(
unsafe { oaknode_clip_set_media_in(dup(&clip), 10, 1) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_clip_get_media_in(dup(&clip), &mut min, &mut mden) },
OAKNODE_OK
);
assert_eq!((min, mden), (10, 1));
let mut speed = 0.0;
assert_eq!(
unsafe { oaknode_clip_get_speed(dup(&clip), &mut speed) },
OAKNODE_OK
);
assert_eq!(speed, 1.0);
assert_eq!(
unsafe { oaknode_clip_set_speed(dup(&clip), 2.0) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_clip_get_speed(dup(&clip), &mut speed) },
OAKNODE_OK
);
assert_eq!(speed, 2.0);
// Transition offsets (non-transition clip -> E_INVALID).
assert_eq!(
unsafe { oaknode_transition_get_in_offset(dup(&clip), &mut min, &mut mden) },
OAKNODE_E_STATE
);
// Failure paths.
assert_eq!(
unsafe { oaknode_block_get_length(CHandle::null(), &mut len_num, &mut len_den) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_block_get_kind(CHandle::null(), &mut kind) },
OAKNODE_E_INVALID
);
free(&mut back);
free(&mut node);
free(&mut clip);
free(&mut gap);
}
/// Footage family: create/filename/proxy + failure paths.
#[test]
fn footage_family() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut footage = unsafe { oaknode_footage_create(dup(&p), cs("/tmp/media.mov").as_ptr()) };
assert!(!footage.ctx.is_null());
assert!(
unsafe { oaknode_footage_create(CHandle::null(), cs("x").as_ptr()) }
.ctx
.is_null()
);
// Filename round-trip (two-stage).
let name = two_stage(|b, s| unsafe { oaknode_footage_filename(dup(&footage), b, s) }).unwrap();
assert_eq!(name, "/tmp/media.mov");
assert_eq!(
unsafe { oaknode_footage_set_filename(dup(&footage), cs("/new.mov").as_ptr()) },
OAKNODE_OK
);
let name = two_stage(|b, s| unsafe { oaknode_footage_filename(dup(&footage), b, s) }).unwrap();
assert_eq!(name, "/new.mov");
assert_eq!(
unsafe { oaknode_footage_filename(CHandle::null(), std::ptr::null_mut(), 0) },
OAKNODE_E_INVALID
);
// Not probed -> invalid.
assert_eq!(unsafe { oaknode_footage_is_valid(dup(&footage)) }, 0);
// as_node cast.
let mut node = unsafe { oaknode_footage_as_node(dup(&footage)) };
assert!(!node.ctx.is_null());
// Proxy fields.
assert_eq!(
unsafe { oaknode_footage_set_proxy(dup(&footage), cs("/proxy.mov").as_ptr(), 2, 0, 1, 1,) },
OAKNODE_OK
);
let proxy =
two_stage(|b, s| unsafe { oaknode_footage_proxy_path(dup(&footage), b, s) }).unwrap();
assert_eq!(proxy, "/proxy.mov");
assert_eq!(
unsafe { oaknode_footage_clear_proxy(dup(&footage)) },
OAKNODE_OK
);
let proxy =
two_stage(|b, s| unsafe { oaknode_footage_proxy_path(dup(&footage), b, s) }).unwrap();
assert_eq!(proxy, "");
free(&mut node);
free(&mut footage);
unsafe { oaknode_project_free(&mut p) };
}
/// Colormanager family: state + config-dependent E_STATE paths.
#[test]
fn colormanager_family() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
let mut cm = unsafe { oaknode_colormanager_init(dup(&p)) };
assert!(!cm.ctx.is_null());
// State without a config: listings return E_STATE.
let mut count = 0;
assert_eq!(
unsafe { oaknode_colormanager_get_display_count(dup(&cm), &mut count) },
OAKNODE_E_STATE
);
// Filename round-trip.
assert_eq!(
unsafe { oaknode_colormanager_set_config_filename(dup(&cm), cs("/cfg.ocio").as_ptr()) },
OAKNODE_OK
);
let name =
two_stage(|b, s| unsafe { oaknode_colormanager_get_config_filename(dup(&cm), b, s) })
.unwrap();
assert_eq!(name, "/cfg.ocio");
assert_eq!(
unsafe {
oaknode_colormanager_get_config_filename(CHandle::null(), std::ptr::null_mut(), 0)
},
OAKNODE_E_INVALID
);
// initialize marks the config loaded (built-in defaults without
// oakrender).
assert_eq!(
unsafe { oaknode_colormanager_initialize(dup(&cm)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_colormanager_get_display_count(dup(&cm), &mut count) },
OAKNODE_OK
);
assert_eq!(count, 1, "built-in default display");
// get_native is a C++-only bridge export: NULL in the Rust port
// (NULL-safe), for both a live and an empty handle.
assert!(unsafe { oaknode_colormanager_get_native(dup(&cm)) }.is_null());
assert!(unsafe { oaknode_colormanager_get_native(CHandle::null()) }.is_null());
free(&mut cm);
unsafe { oaknode_project_free(&mut p) };
}
/// Traverser family: database generation + row enumeration + failures.
#[test]
fn traverser_family() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
// A math node in the project.
let mut math = unsafe {
oaknode::ffi::factory::oaknode_factory_create_from_id(
cs("org.olivevideoeditor.Olive.math").as_ptr(),
)
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&math)) },
OAKNODE_OK
);
let mut tr = unsafe { oaknode_traverser_init() };
assert!(!tr.ctx.is_null());
let mut db = CHandle::null();
assert_eq!(
unsafe { oaknode_traverser_generate_database(dup(&tr), dup(&math), 0, 1, 1, 1, &mut db) },
OAKNODE_OK
);
assert!(!db.ctx.is_null());
let mut rows = 0;
assert_eq!(
unsafe { oaknode_traverser_database_row_count(dup(&db), &mut rows) },
OAKNODE_OK
);
assert_eq!(rows, 1, "no input edges, so only the root row");
// Failure: unknown database handle.
assert_eq!(
unsafe { oaknode_traverser_database_row_count(CHandle::null(), &mut rows) },
OAKNODE_E_INVALID
);
unsafe { oaknode_traverser_database_free(&mut db) };
free(&mut tr);
free(&mut math);
unsafe { oaknode_project_free(&mut p) };
}
/// Serializer family: initialize/save-to-xml/load round-trip + failures.
///
/// Needs the `test-stubs` feature: `save_to_xml`/`load_from_xml` route
/// through the oakcommon XML bridge, whose symbols only resolve in the
/// test binary when the in-crate stubs are compiled in.
#[cfg(feature = "test-stubs")]
#[test]
fn serializer_family() {
let _g = lock();
assert_eq!(unsafe { oaknode_serializer_initialize() }, OAKNODE_OK);
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut sd = unsafe { oaknode_serializer_savedata_create(1, dup(&p)) }; // LOAD_ONLY_NODES
assert!(!sd.ctx.is_null());
let xml = two_stage(|b, s| unsafe { oaknode_serializer_save_to_xml(dup(&sd), b, s) }).unwrap();
assert!(xml.contains("<project"), "serializes a project document");
assert_eq!(
unsafe { oaknode_serializer_save_to_xml(CHandle::null(), std::ptr::null_mut(), 0) },
OAKNODE_E_INVALID
);
// Load back into a fresh project.
let mut p2 = unsafe { oaknode_project_init() };
let mut result = -1;
let mut ld = CHandle::null();
let xml_c = cs(&xml);
assert_eq!(
unsafe {
oaknode_serializer_load_from_xml(
dup(&p2),
xml_c.as_ptr(),
1,
&mut result,
&mut ld,
std::ptr::null_mut(),
0,
)
},
OAKNODE_OK
);
assert_eq!(result, 0, "OAKNODE_SERIALIZER_OK");
assert!(!ld.ctx.is_null());
let mut count = 0;
assert_eq!(
unsafe { oaknode_serializer_loaddata_node_count(dup(&ld)) },
1
);
let _ = count;
// node_at for the loaded node.
let mut loaded_node = unsafe { oaknode_serializer_loaddata_node_at(dup(&ld), 0) };
assert!(!loaded_node.ctx.is_null());
assert!(unsafe { oaknode_serializer_loaddata_node_at(dup(&ld), 5) }
.ctx
.is_null());
// Corrupt XML -> SERIALIZER_XML_ERROR result, empty load data.
let mut result2 = -1;
let mut ld2 = CHandle::null();
let corrupt = cs("<<<not xml");
assert_eq!(
unsafe {
oaknode_serializer_load_from_xml(
dup(&p2),
corrupt.as_ptr(),
1,
&mut result2,
&mut ld2,
std::ptr::null_mut(),
0,
)
},
OAKNODE_OK
);
assert_eq!(result2, 5, "OAKNODE_SERIALIZER_XML_ERROR");
assert!(ld2.ctx.is_null());
// save_to_file / load_from_file round-trip into a temp file.
let path =
std::env::temp_dir().join(format!("oaknode-serializer-{}.oakproj", std::process::id()));
let path_c = cs(path.to_str().unwrap());
let mut file_code = -1;
assert_eq!(
unsafe {
oaknode_serializer_save_to_file(
dup(&p),
path_c.as_ptr(),
0,
&mut file_code,
std::ptr::null_mut(),
0,
)
},
OAKNODE_OK
);
assert_eq!(file_code, 0, "OAKNODE_SERIALIZER_OK");
assert!(path.exists());
let mut p3 = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p3)) }, OAKNODE_OK);
let mut file_code = -1;
assert_eq!(
unsafe {
oaknode_serializer_load_from_file(
dup(&p3),
path_c.as_ptr(),
&mut file_code,
std::ptr::null_mut(),
0,
)
},
OAKNODE_OK
);
assert_eq!(file_code, 0);
// File variants fail on a missing file (FILE_ERROR) and on bad args.
let missing = cs("/nonexistent/oaknode-serializer-missing.oakproj");
let mut file_code = -1;
assert_eq!(
unsafe {
oaknode_serializer_load_from_file(
dup(&p3),
missing.as_ptr(),
&mut file_code,
std::ptr::null_mut(),
0,
)
},
oaknode::error::OAKNODE_E_FAILED
);
assert_eq!(file_code, 4, "OAKNODE_SERIALIZER_FILE_ERROR");
assert_eq!(
unsafe {
oaknode_serializer_save_to_file(
CHandle::null(),
path_c.as_ptr(),
0,
&mut file_code,
std::ptr::null_mut(),
0,
)
},
OAKNODE_E_INVALID
);
let _ = std::fs::remove_file(&path);
free(&mut loaded_node);
unsafe { oaknode_serializer_loaddata_free(&mut ld) };
free(&mut sd);
unsafe { oaknode_project_free(&mut p) };
unsafe { oaknode_project_free(&mut p2) };
unsafe { oaknode_project_free(&mut p3) };
}
/// Keyframe handle family, live paths: create/get/set +
/// failures. No undo bridge involved.
#[test]
fn keyframe_family_basic() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut math = unsafe {
oaknode::ffi::factory::oaknode_factory_create_from_id(
cs("org.olivevideoeditor.Olive.math").as_ptr(),
)
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&math)) },
OAKNODE_OK
);
let f = |x: f64| OakNodeValue {
kind: oak::FLOAT,
num: 0,
den: 0,
f: [x, 0.0, 0.0, 0.0],
};
// create with a parent; invalid type and string values are rejected.
let mut kf = unsafe {
oaknode_keyframe_create(
1,
25,
&f(3.5),
0,
0,
-1,
cs("param_a_in").as_ptr(),
dup(&math),
)
};
assert!(!kf.ctx.is_null());
assert!(unsafe {
oaknode_keyframe_create(
0,
1,
&f(1.0),
99,
0,
-1,
cs("param_a_in").as_ptr(),
dup(&math),
)
}
.ctx
.is_null());
let sv = OakNodeValue {
kind: 10,
num: 0,
den: 0,
f: [0.0; 4],
};
assert!(unsafe {
oaknode_keyframe_create(0, 1, &sv, 0, 0, -1, cs("param_a_in").as_ptr(), dup(&math))
}
.ctx
.is_null());
// get_time / set_time (live).
let (mut num, mut den) = (0i64, 0i64);
assert_eq!(
unsafe { oaknode_keyframe_get_time(dup(&kf), &mut num, &mut den) },
OAKNODE_OK
);
assert_eq!((num, den), (1, 25));
assert_eq!(
unsafe { oaknode_keyframe_get_time(CHandle::null(), &mut num, &mut den) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_keyframe_set_time(dup(&kf), 2, 25) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_keyframe_get_time(dup(&kf), &mut num, &mut den) },
OAKNODE_OK
);
assert_eq!((num, den), (2, 25));
// get_value / set_value (live).
let mut out = OakNodeValue::none();
assert_eq!(
unsafe { oaknode_keyframe_get_value(dup(&kf), &mut out) },
OAKNODE_OK
);
assert_eq!(out.kind, oak::FLOAT);
assert_eq!(out.f[0], 3.5);
assert_eq!(
unsafe { oaknode_keyframe_get_value(CHandle::null(), &mut out) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_keyframe_set_value(dup(&kf), &f(7.0)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_keyframe_get_value(dup(&kf), &mut out) },
OAKNODE_OK
);
assert_eq!(out.f[0], 7.0);
// type round-trip; switching to bezier mints the default handles.
let mut ty = -1;
assert_eq!(
unsafe { oaknode_keyframe_get_type(dup(&kf), &mut ty) },
OAKNODE_OK
);
assert_eq!(ty, 0, "linear");
assert_eq!(
unsafe { oaknode_keyframe_set_type(dup(&kf), 2) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_keyframe_get_type(dup(&kf), &mut ty) },
OAKNODE_OK
);
assert_eq!(ty, 2, "bezier");
let (mut bx, mut by) = (0.0, 0.0);
assert_eq!(
unsafe { oaknode_keyframe_get_bezier_control(dup(&kf), 0, &mut bx, &mut by) },
OAKNODE_OK
);
assert_eq!((bx, by), (-1.0, 0.0), "default in-handle");
assert_eq!(
unsafe { oaknode_keyframe_get_bezier_control(dup(&kf), 1, &mut bx, &mut by) },
OAKNODE_OK
);
assert_eq!((bx, by), (1.0, 0.0), "default out-handle");
assert_eq!(
unsafe { oaknode_keyframe_get_bezier_control(dup(&kf), 5, &mut bx, &mut by) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_keyframe_set_bezier_control(dup(&kf), 0, -0.4, 1.2) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_keyframe_get_valid_bezier_control(dup(&kf), 0, &mut bx, &mut by) },
OAKNODE_OK
);
assert_eq!(
(bx, by),
(-0.4, 1.2),
"standalone handles are already valid"
);
// track / element / input / parent.
let mut trk = -1;
assert_eq!(
unsafe { oaknode_keyframe_get_track(dup(&kf), &mut trk) },
OAKNODE_OK
);
assert_eq!(trk, 0);
let mut el = -9;
assert_eq!(
unsafe { oaknode_keyframe_get_element(dup(&kf), &mut el) },
OAKNODE_OK
);
assert_eq!(el, -1);
let input = two_stage(|b, s| unsafe { oaknode_keyframe_get_input(dup(&kf), b, s) }).unwrap();
assert_eq!(input, "param_a_in");
let mut parent = CHandle::null();
assert_eq!(
unsafe { oaknode_keyframe_get_parent(dup(&kf), &mut parent) },
OAKNODE_OK
);
assert!(!parent.ctx.is_null());
// An orphaned keyframe reports an empty parent.
let mut kf2 = unsafe {
oaknode_keyframe_create(
0,
1,
&f(1.0),
0,
0,
-1,
cs("param_a_in").as_ptr(),
CHandle::null(),
)
};
let mut parent2 = CHandle::null();
assert_eq!(
unsafe { oaknode_keyframe_get_parent(dup(&kf2), &mut parent2) },
OAKNODE_OK
);
assert!(parent2.ctx.is_null());
// value_string round-trip.
assert_eq!(
unsafe { oaknode_keyframe_set_value_string(dup(&kf), cs("hello").as_ptr()) },
OAKNODE_OK
);
let s =
two_stage(|b, sz| unsafe { oaknode_keyframe_get_value_string(dup(&kf), b, sz) }).unwrap();
assert_eq!(s, "hello");
// opposing bezier type.
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(7) },
OAKNODE_E_INVALID
);
// has_sibling_at_time: the parent's track is empty -> 0.
let mut sib = -1;
assert_eq!(
unsafe { oaknode_keyframe_has_sibling_at_time(dup(&kf), 1, 25, &mut sib) },
OAKNODE_OK
);
assert_eq!(sib, 0);
assert_eq!(
unsafe { oaknode_keyframe_has_sibling_at_time(CHandle::null(), 1, 25, &mut sib) },
OAKNODE_E_INVALID
);
// compute_paste_value against the parent's input (declared float).
let mut pv = OakNodeValue::none();
assert_eq!(
unsafe { oaknode_keyframe_compute_paste_value(dup(&math), dup(&kf), &mut pv) },
OAKNODE_OK
);
assert_eq!(pv.kind, oak::FLOAT);
// Unknown input on the target -> NOT_FOUND.
let mut kf3 = unsafe {
oaknode_keyframe_create(0, 1, &f(1.0), 0, 0, -1, cs("nope_in").as_ptr(), dup(&math))
};
assert_eq!(
unsafe { oaknode_keyframe_compute_paste_value(dup(&math), dup(&kf3), &mut pv) },
OAKNODE_E_NOT_FOUND
);
free(&mut parent);
free(&mut parent2);
unsafe { oaknode_keyframe_free(&mut kf3) };
unsafe { oaknode_keyframe_free(&mut kf2) };
unsafe { oaknode_keyframe_free(&mut kf) };
free(&mut math);
unsafe { oaknode_project_free(&mut p) };
}
/// Keyframe handle family, undoable paths: setter commands round-trip
/// through the test-stub undo bridge.
#[cfg(feature = "test-stubs")]
#[test]
fn keyframe_undoable() {
let _g = lock();
let f = |x: f64| OakNodeValue {
kind: oak::FLOAT,
num: 0,
den: 0,
f: [x, 0.0, 0.0, 0.0],
};
let mut kf = unsafe {
oaknode_keyframe_create(
0,
1,
&f(1.0),
0,
0,
-1,
cs("param_a_in").as_ptr(),
CHandle::null(),
)
};
// set_time_undoable.
let mut cmd = CHandle::null();
assert_eq!(
unsafe { oaknode_keyframe_set_time_undoable(dup(&kf), 5, 1, &mut cmd) },
OAKNODE_OK
);
assert!(!cmd.ctx.is_null());
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
let (mut num, mut den) = (0i64, 0i64);
assert_eq!(
unsafe { oaknode_keyframe_get_time(dup(&kf), &mut num, &mut den) },
OAKNODE_OK
);
assert_eq!((num, den), (5, 1));
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe { oaknode_keyframe_get_time(dup(&kf), &mut num, &mut den) },
OAKNODE_OK
);
assert_eq!((num, den), (0, 1));
unsafe { oakundo_command_free(&mut cmd) };
// set_value_undoable.
let mut cmd = CHandle::null();
assert_eq!(
unsafe { oaknode_keyframe_set_value_undoable(dup(&kf), &f(9.0), &mut cmd) },
OAKNODE_OK
);
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
let mut out = OakNodeValue::none();
assert_eq!(
unsafe { oaknode_keyframe_get_value(dup(&kf), &mut out) },
OAKNODE_OK
);
assert_eq!(out.f[0], 9.0);
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe { oaknode_keyframe_get_value(dup(&kf), &mut out) },
OAKNODE_OK
);
assert_eq!(out.f[0], 1.0);
unsafe { oakundo_command_free(&mut cmd) };
// set_type_undoable (linear -> hold -> back).
let mut cmd = CHandle::null();
assert_eq!(
unsafe { oaknode_keyframe_set_type_undoable(dup(&kf), 1, &mut cmd) },
OAKNODE_OK
);
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
let mut ty = -1;
assert_eq!(
unsafe { oaknode_keyframe_get_type(dup(&kf), &mut ty) },
OAKNODE_OK
);
assert_eq!(ty, 1);
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe { oaknode_keyframe_get_type(dup(&kf), &mut ty) },
OAKNODE_OK
);
assert_eq!(ty, 0);
unsafe { oakundo_command_free(&mut cmd) };
// set_bezier_control_undoable.
let mut cmd = CHandle::null();
assert_eq!(
unsafe { oaknode_keyframe_set_bezier_control_undoable(dup(&kf), 0, -0.5, 2.0, &mut cmd) },
OAKNODE_OK
);
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
let (mut bx, mut by) = (0.0, 0.0);
assert_eq!(
unsafe { oaknode_keyframe_get_bezier_control(dup(&kf), 0, &mut bx, &mut by) },
OAKNODE_OK
);
assert_eq!((bx, by), (-0.5, 2.0));
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe { oaknode_keyframe_get_bezier_control(dup(&kf), 0, &mut bx, &mut by) },
OAKNODE_OK
);
assert_eq!((bx, by), (0.0, 0.0));
unsafe { oakundo_command_free(&mut cmd) };
// set_value_string_undoable.
let mut cmd = CHandle::null();
assert_eq!(
unsafe {
oaknode_keyframe_set_value_string_undoable(dup(&kf), cs("str").as_ptr(), &mut cmd)
},
OAKNODE_OK
);
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
let s =
two_stage(|b, sz| unsafe { oaknode_keyframe_get_value_string(dup(&kf), b, sz) }).unwrap();
assert_eq!(s, "str");
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
let s =
two_stage(|b, sz| unsafe { oaknode_keyframe_get_value_string(dup(&kf), b, sz) }).unwrap();
assert_eq!(s, "1", "float 1.0 after undo");
unsafe { oaknode_keyframe_free(&mut kf) };
}
/// Group family, live paths: create/cast/passthroughs/output/resolve +
/// failures. No undo bridge involved.
#[test]
fn group_family_basic() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut group = unsafe { oaknode_group_create() };
assert!(!group.ctx.is_null());
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&group)) },
OAKNODE_OK
);
let mut math = unsafe {
oaknode::ffi::factory::oaknode_factory_create_from_id(
cs("org.olivevideoeditor.Olive.math").as_ptr(),
)
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&math)) },
OAKNODE_OK
);
// cast: the group casts; a non-group node and empty handles do not.
let mut cast = unsafe { oaknode_group_cast(dup(&group)) };
assert!(!cast.ctx.is_null());
assert!(unsafe { oaknode_group_cast(dup(&math)) }.ctx.is_null());
assert!(unsafe { oaknode_group_cast(CHandle::null()) }.ctx.is_null());
free(&mut cast);
// Add a passthrough for math's param_a_in; repeat returns the same id.
let id = two_stage(|b, s| unsafe {
oaknode_group_add_input_passthrough(
dup(&group),
dup(&math),
cs("param_a_in").as_ptr(),
-1,
b,
s,
)
})
.unwrap();
assert_eq!(id, "param_a_in");
let id2 = two_stage(|b, s| unsafe {
oaknode_group_add_input_passthrough(
dup(&group),
dup(&math),
cs("param_a_in").as_ptr(),
-1,
b,
s,
)
})
.unwrap();
assert_eq!(id2, "param_a_in");
// Unknown inner input -> NOT_FOUND.
assert_eq!(
unsafe {
oaknode_group_add_input_passthrough(
dup(&group),
dup(&math),
cs("nope_in").as_ptr(),
-1,
std::ptr::null_mut(),
0,
)
},
OAKNODE_E_NOT_FOUND
);
// Cross-project node -> INVALID.
let mut p2 = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p2)) }, OAKNODE_OK);
let mut math2 = unsafe {
oaknode::ffi::factory::oaknode_factory_create_from_id(
cs("org.olivevideoeditor.Olive.math").as_ptr(),
)
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p2), dup(&math2)) },
OAKNODE_OK
);
assert_eq!(
unsafe {
oaknode_group_add_input_passthrough(
dup(&group),
dup(&math2),
cs("param_a_in").as_ptr(),
-1,
std::ptr::null_mut(),
0,
)
},
OAKNODE_E_INVALID
);
// count / id_at / input_at.
let mut count = 0;
assert_eq!(
unsafe { oaknode_group_passthrough_count(dup(&group), &mut count) },
OAKNODE_OK
);
assert_eq!(count, 1);
let pid =
two_stage(|b, s| unsafe { oaknode_group_passthrough_id_at(dup(&group), 0, b, s) }).unwrap();
assert_eq!(pid, "param_a_in");
assert_eq!(
unsafe { oaknode_group_passthrough_id_at(dup(&group), 5, std::ptr::null_mut(), 0) },
OAKNODE_E_NOT_FOUND
);
let mut inner_node = CHandle::null();
let mut inner_el = -9;
let iid = two_stage(|b, s| unsafe {
oaknode_group_passthrough_input_at(dup(&group), 0, &mut inner_node, b, s, &mut inner_el)
})
.unwrap();
assert_eq!(iid, "param_a_in");
assert!(!inner_node.ctx.is_null());
assert_eq!(inner_el, -1);
free(&mut inner_node);
// remove_input_passthrough: second removal is NOT_FOUND.
assert_eq!(
unsafe {
oaknode_group_remove_input_passthrough(
dup(&group),
dup(&math),
cs("param_a_in").as_ptr(),
-1,
)
},
OAKNODE_OK
);
assert_eq!(
unsafe {
oaknode_group_remove_input_passthrough(
dup(&group),
dup(&math),
cs("param_a_in").as_ptr(),
-1,
)
},
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe { oaknode_group_passthrough_count(dup(&group), &mut count) },
OAKNODE_OK
);
assert_eq!(count, 0);
// output passthrough set/get + clear with an empty handle.
assert_eq!(
unsafe { oaknode_group_set_output_passthrough(dup(&group), dup(&math)) },
OAKNODE_OK
);
let mut out = CHandle::null();
assert_eq!(
unsafe { oaknode_group_get_output_passthrough(dup(&group), &mut out) },
OAKNODE_OK
);
assert!(!out.ctx.is_null());
free(&mut out);
assert_eq!(
unsafe { oaknode_group_set_output_passthrough(dup(&group), CHandle::null()) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_group_get_output_passthrough(dup(&group), &mut out) },
OAKNODE_OK
);
assert!(out.ctx.is_null());
// resolve_input: a non-group input resolves to itself.
let mut rn = CHandle::null();
let mut re = -9;
let rid = two_stage(|b, s| unsafe {
oaknode_group_resolve_input(
dup(&math),
cs("param_a_in").as_ptr(),
-1,
&mut rn,
b,
s,
&mut re,
)
})
.unwrap();
assert_eq!(rid, "param_a_in");
assert!(!rn.ctx.is_null());
assert_eq!(re, -1);
free(&mut rn);
// Unresolvable input -> NOT_FOUND.
assert_eq!(
unsafe {
oaknode_group_resolve_input(
dup(&math),
cs("nope_in").as_ptr(),
-1,
&mut rn,
std::ptr::null_mut(),
0,
&mut re,
)
},
OAKNODE_E_NOT_FOUND
);
unsafe { oaknode_group_free(&mut group) };
free(&mut math2);
free(&mut math);
unsafe { oaknode_project_free(&mut p2) };
unsafe { oaknode_project_free(&mut p) };
}
/// Group family, undoable paths: passthrough/output commands round-trip
/// through the test-stub undo bridge.
#[cfg(feature = "test-stubs")]
#[test]
fn group_undoable() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut group = unsafe { oaknode_group_create() };
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&group)) },
OAKNODE_OK
);
let mut math = unsafe {
oaknode::ffi::factory::oaknode_factory_create_from_id(
cs("org.olivevideoeditor.Olive.math").as_ptr(),
)
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&math)) },
OAKNODE_OK
);
// add_input_passthrough_undoable: redo adds, undo removes.
let mut cmd = CHandle::null();
assert_eq!(
unsafe {
oaknode_group_add_input_passthrough_undoable(
dup(&group),
dup(&math),
cs("param_a_in").as_ptr(),
-1,
&mut cmd,
)
},
OAKNODE_OK
);
assert!(!cmd.ctx.is_null());
let mut count = 0;
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe { oaknode_group_passthrough_count(dup(&group), &mut count) },
OAKNODE_OK
);
assert_eq!(count, 1);
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe { oaknode_group_passthrough_count(dup(&group), &mut count) },
OAKNODE_OK
);
assert_eq!(count, 0);
unsafe { oakundo_command_free(&mut cmd) };
// set_output_passthrough_undoable: redo sets, undo restores.
let mut cmd = CHandle::null();
assert_eq!(
unsafe { oaknode_group_set_output_passthrough_undoable(dup(&group), dup(&math), &mut cmd) },
OAKNODE_OK
);
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
let mut out = CHandle::null();
assert_eq!(
unsafe { oaknode_group_get_output_passthrough(dup(&group), &mut out) },
OAKNODE_OK
);
assert!(!out.ctx.is_null());
free(&mut out);
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe { oaknode_group_get_output_passthrough(dup(&group), &mut out) },
OAKNODE_OK
);
assert!(out.ctx.is_null());
unsafe { oakundo_command_free(&mut cmd) };
unsafe { oaknode_group_free(&mut group) };
free(&mut math);
unsafe { oaknode_project_free(&mut p) };
}
/// Dragger family: create/start/drag/end/is_started + failures. `end`
/// builds undo commands through the test-stub bridge.
#[cfg(feature = "test-stubs")]
#[test]
fn dragger_family() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut math = unsafe {
oaknode::ffi::factory::oaknode_factory_create_from_id(
cs("org.olivevideoeditor.Olive.math").as_ptr(),
)
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&math)) },
OAKNODE_OK
);
let mut dg = unsafe { oaknode_dragger_create(dup(&math), cs("param_a_in").as_ptr(), -1, 0) };
assert!(!dg.ctx.is_null());
// Unknown input -> empty handle.
assert!(
unsafe { oaknode_dragger_create(dup(&math), cs("nope_in").as_ptr(), -1, 0) }
.ctx
.is_null()
);
let mut started = -1;
assert_eq!(
unsafe { oaknode_dragger_is_started(dup(&dg), &mut started) },
OAKNODE_OK
);
assert_eq!(started, 0);
// Negative track -> E_INVALID (before start).
let mut dg2 = unsafe { oaknode_dragger_create(dup(&math), cs("param_a_in").as_ptr(), -1, 0) };
assert_eq!(
unsafe { oaknode_dragger_start(dup(&dg2), 0, 1, -1, 1) },
OAKNODE_E_INVALID
);
// start at time 0 on track 0; double start -> E_STATE.
assert_eq!(
unsafe { oaknode_dragger_start(dup(&dg), 0, 1, 0, 1) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_dragger_is_started(dup(&dg), &mut started) },
OAKNODE_OK
);
assert_eq!(started, 1);
assert_eq!(
unsafe { oaknode_dragger_start(dup(&dg), 1, 1, 0, 1) },
OAKNODE_E_STATE
);
// drag sets the value live; wrong POD kind -> E_INVALID.
let f = |x: f64| OakNodeValue {
kind: oak::FLOAT,
num: 0,
den: 0,
f: [x, 0.0, 0.0, 0.0],
};
assert_eq!(
unsafe { oaknode_dragger_drag(dup(&dg), &f(4.0)) },
OAKNODE_OK
);
let mut out = OakNodeValue::none();
assert_eq!(
unsafe {
oaknode_node_get_input_at_time(dup(&math), cs("param_a_in").as_ptr(), 0, 1, &mut out)
},
OAKNODE_OK
);
assert_eq!(out.kind, oak::FLOAT);
assert_eq!(out.f[0], 4.0);
let bad = OakNodeValue {
kind: oak::INT,
num: 3,
den: 0,
f: [0.0; 4],
};
assert_eq!(
unsafe { oaknode_dragger_drag(dup(&dg), &bad) },
OAKNODE_E_INVALID
);
// drag before start -> E_STATE.
assert_eq!(
unsafe { oaknode_dragger_drag(dup(&dg2), &f(1.0)) },
OAKNODE_E_STATE
);
// end builds a multi command; the command undoes and redoes the drag.
let mut cmd = CHandle::null();
assert_eq!(
unsafe { oaknode_dragger_end(dup(&dg), &mut cmd) },
OAKNODE_OK
);
assert!(!cmd.ctx.is_null());
assert_eq!(
unsafe { oaknode_dragger_is_started(dup(&dg), &mut started) },
OAKNODE_OK
);
assert_eq!(started, 0, "end resets the dragger");
assert_eq!(
unsafe { oaknode_dragger_end(dup(&dg), &mut cmd) },
OAKNODE_E_STATE
);
// The drag was applied live, so the command round-trips
// redo -> undo -> redo.
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe {
oaknode_node_get_input_at_time(dup(&math), cs("param_a_in").as_ptr(), 0, 1, &mut out)
},
OAKNODE_OK
);
assert_eq!(out.f[0], 4.0, "redo re-applies the drag");
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe {
oaknode_node_get_input_at_time(dup(&math), cs("param_a_in").as_ptr(), 0, 1, &mut out)
},
OAKNODE_OK
);
assert_eq!(out.f[0], 0.0, "undo restores the pre-drag value");
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe {
oaknode_node_get_input_at_time(dup(&math), cs("param_a_in").as_ptr(), 0, 1, &mut out)
},
OAKNODE_OK
);
assert_eq!(out.f[0], 4.0, "redo after undo re-applies the drag");
unsafe { oakundo_command_free(&mut cmd) };
// Null args -> E_INVALID.
assert_eq!(
unsafe { oaknode_dragger_is_started(CHandle::null(), &mut started) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_dragger_drag(CHandle::null(), &f(1.0)) },
OAKNODE_E_INVALID
);
unsafe { oaknode_dragger_free(&mut dg2) };
unsafe { oaknode_dragger_free(&mut dg) };
free(&mut math);
unsafe { oaknode_project_free(&mut p) };
}
/// Undoable node exports: create commands, redo/undo round-trip via the
/// test-stub undo bridge.
///
/// Needs the `test-stubs` feature: the undo commands are created through
/// the oakundo bridge, whose `oakundo_command_init` symbol only resolves
/// in the test binary when the in-crate stubs are compiled in.
#[cfg(feature = "test-stubs")]
#[test]
fn undoable_node_exports() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut math = unsafe {
oaknode::ffi::factory::oaknode_factory_create_from_id(
cs("org.olivevideoeditor.Olive.math").as_ptr(),
)
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&math)) },
OAKNODE_OK
);
// set_label_undoable: redo sets, undo restores.
let mut cmd = CHandle::null();
assert_eq!(
unsafe { oaknode_node_set_label_undoable(dup(&math), cs("new label").as_ptr(), &mut cmd) },
OAKNODE_OK
);
assert!(!cmd.ctx.is_null());
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
let label =
two_stage(|b, s| unsafe { oaknode::ffi::node::oaknode_node_get_label(dup(&math), b, s) })
.unwrap();
assert_eq!(label, "new label");
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
let label =
two_stage(|b, s| unsafe { oaknode::ffi::node::oaknode_node_get_label(dup(&math), b, s) })
.unwrap();
assert_eq!(label, "");
unsafe { oakundo_command_free(&mut cmd) };
// set_enabled_undoable.
let mut cmd = CHandle::null();
assert_eq!(
unsafe { oaknode_node_set_enabled_undoable(dup(&math), 0, &mut cmd) },
OAKNODE_OK
);
let mut en = 1;
assert_eq!(
unsafe { oaknode::ffi::node::oaknode_node_is_enabled(dup(&math), &mut en) },
OAKNODE_OK
);
assert_eq!(en, 1);
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe { oaknode::ffi::node::oaknode_node_is_enabled(dup(&math), &mut en) },
OAKNODE_OK
);
assert_eq!(en, 0);
unsafe { oakundo_command_free(&mut cmd) };
// set_input_undoable on param_a_in.
let mut cmd = CHandle::null();
let v = OakNodeValue {
kind: oak::FLOAT,
num: 0,
den: 0,
f: [7.5, 0.0, 0.0, 0.0],
};
assert_eq!(
unsafe {
oaknode_node_set_input_undoable(dup(&math), cs("param_a_in").as_ptr(), &v, &mut cmd)
},
OAKNODE_OK
);
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
let mut pod = OakNodeValue::none();
assert_eq!(
unsafe { oaknode_node_get_input(dup(&math), cs("param_a_in").as_ptr(), &mut pod) },
OAKNODE_OK
);
assert_eq!(pod.f[0], 7.5);
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe { oaknode_node_get_input(dup(&math), cs("param_a_in").as_ptr(), &mut pod) },
OAKNODE_OK
);
assert_eq!(pod.f[0], 0.0);
unsafe { oakundo_command_free(&mut cmd) };
// set_input_at_time_undoable: inserts a keyframe on redo.
let mut cmd = CHandle::null();
let kv = OakNodeValue {
kind: oak::FLOAT,
num: 0,
den: 0,
f: [3.0, 0.0, 0.0, 0.0],
};
assert_eq!(
unsafe {
oaknode_node_set_input_at_time_undoable(
dup(&math),
cs("param_a_in").as_ptr(),
10,
1,
&kv,
0,
&mut cmd,
)
},
OAKNODE_OK
);
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
// At time 10 the value is now keyframed to 3.0.
let mut at = OakNodeValue::none();
assert_eq!(
unsafe {
oaknode::ffi::node::oaknode_node_get_input_at_time(
dup(&math),
cs("param_a_in").as_ptr(),
10,
1,
&mut at,
)
},
OAKNODE_OK
);
assert_eq!(at.f[0], 3.0);
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe {
oaknode::ffi::node::oaknode_node_get_input_at_time(
dup(&math),
cs("param_a_in").as_ptr(),
10,
1,
&mut at,
)
},
OAKNODE_OK
);
assert_eq!(at.f[0], 0.0, "keyframe removed on undo");
unsafe { oakundo_command_free(&mut cmd) };
// connect_undoable: redo connects, undo disconnects.
let mut other = unsafe {
oaknode::ffi::factory::oaknode_factory_create_from_id(
cs("org.olivevideoeditor.Olive.math").as_ptr(),
)
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&other)) },
OAKNODE_OK
);
let mut cmd = CHandle::null();
assert_eq!(
unsafe {
oaknode_node_connect_undoable(
dup(&math),
dup(&other),
cs("param_a_in").as_ptr(),
&mut cmd,
)
},
OAKNODE_OK
);
assert_eq!(unsafe { oakundo_command_redo_now(dup(&cmd)) }.unwrap(), 0);
let mut conn = 0;
assert_eq!(
unsafe {
oaknode::ffi::node::oaknode_node_input_is_connected(
dup(&other),
cs("param_a_in").as_ptr(),
&mut conn,
)
},
OAKNODE_OK
);
assert_eq!(conn, 1);
assert_eq!(unsafe { oakundo_command_undo_now(dup(&cmd)) }.unwrap(), 0);
assert_eq!(
unsafe {
oaknode::ffi::node::oaknode_node_input_is_connected(
dup(&other),
cs("param_a_in").as_ptr(),
&mut conn,
)
},
OAKNODE_OK
);
assert_eq!(conn, 0);
unsafe { oakundo_command_free(&mut cmd) };
// disconnect_undoable failure: unconnected input.
let mut cmd = CHandle::null();
assert_eq!(
unsafe {
oaknode_node_disconnect_undoable(dup(&other), cs("param_a_in").as_ptr(), &mut cmd)
},
OAKNODE_E_NOT_FOUND
);
free(&mut other);
free(&mut math);
unsafe { oaknode_project_free(&mut p) };
}
// The undo stub functions are resolved through the dlsym path (the
// test-stubs feature compiles them into the test binary); only the
// undoable-node test above uses them.
#[cfg(feature = "test-stubs")]
use oaknode::bridge::undo::{
command_free as oakundo_command_free, command_redo_now as oakundo_command_redo_now,
command_undo_now as oakundo_command_undo_now,
};
/// A minimal multicam-typed behavior for the node-backed multicam
/// exports (the real `MultiCamNode` behavior is a Phase-3 item).
struct FakeMulticam;
impl oaknode::node::NodeBehavior for FakeMulticam {
fn name(&self) -> &str {
"Multi-Cam"
}
fn type_id(&self) -> &str {
"org.olivevideoeditor.Olive.multicam"
}
fn duplicate(
&self,
_core: &oaknode::node::NodeCore,
) -> Option<Box<dyn oaknode::node::NodeBehavior>> {
None
}
}
/// A node handle for an arbitrary behavior/core (unit-style construction
/// for exports whose node type has no factory constructor yet).
fn make_raw_node(
core: oaknode::node::NodeCore,
behavior: Box<dyn oaknode::node::NodeBehavior>,
) -> CHandle {
let project = oaknode::project::Project::new();
let id = {
let mut p = project.lock().unwrap();
p.graph.add_node(core, behavior)
};
oaknode::handle::make_owned(oaknode::project::NodeRef::new(project, id, false))
}
/// Multicam family: input-id strings, grid math incl. 0/1 source and
/// non-square grids, index<->row/col round-trips, node-backed exports.
#[test]
fn multicam_family() {
let _g = lock();
let s = |p: *const c_char| {
unsafe { std::ffi::CStr::from_ptr(p) }
.to_str()
.unwrap()
.to_string()
};
assert_eq!(s(unsafe { oaknode_multicam_input_current() }), "current_in");
assert_eq!(s(unsafe { oaknode_multicam_input_sources() }), "sources_in");
assert_eq!(
s(unsafe { oaknode_multicam_input_sequence() }),
"sequence_in"
);
assert_eq!(
s(unsafe { oaknode_multicam_input_sequence_type() }),
"sequence_type_in"
);
// Grid math (rows/columns): the square-ish cover grid.
let mut rows = 0;
let mut cols = 0;
let grid = |n: c_int, r: &mut c_int, c: &mut c_int| unsafe {
oaknode_multicam_get_rows_and_columns(n, r, c)
};
assert_eq!(grid(0, &mut rows, &mut cols), OAKNODE_OK);
assert_eq!((rows, cols), (1, 1), "0 sources -> 1x1");
assert_eq!(grid(1, &mut rows, &mut cols), OAKNODE_OK);
assert_eq!((rows, cols), (1, 1), "1 source -> 1x1");
assert_eq!(grid(2, &mut rows, &mut cols), OAKNODE_OK);
assert_eq!((rows, cols), (1, 2), "2 sources -> 1x2");
assert_eq!(grid(3, &mut rows, &mut cols), OAKNODE_OK);
assert_eq!((rows, cols), (2, 2), "3 sources -> 2x2");
assert_eq!(grid(4, &mut rows, &mut cols), OAKNODE_OK);
assert_eq!((rows, cols), (2, 2));
assert_eq!(grid(5, &mut rows, &mut cols), OAKNODE_OK);
assert_eq!((rows, cols), (2, 3), "5 sources -> non-square 2x3");
assert_eq!(grid(6, &mut rows, &mut cols), OAKNODE_OK);
assert_eq!((rows, cols), (2, 3), "6 sources -> 2x3");
assert_eq!(grid(10, &mut rows, &mut cols), OAKNODE_OK);
assert_eq!((rows, cols), (3, 4), "10 sources -> 3x4");
// Failure: negative count / null out.
assert_eq!(grid(-1, &mut rows, &mut cols), OAKNODE_E_INVALID);
assert_eq!(
unsafe { oaknode_multicam_get_rows_and_columns(4, std::ptr::null_mut(), &mut cols) },
OAKNODE_E_INVALID
);
// index -> (row, col) and back.
let mut row = -1;
let mut col = -1;
let to_rc = |i: c_int, r: c_int, c: c_int, or: &mut c_int, oc: &mut c_int| unsafe {
oaknode_multicam_index_to_row_cols(i, r, c, or, oc)
};
assert_eq!(to_rc(0, 2, 3, &mut row, &mut col), OAKNODE_OK);
assert_eq!((row, col), (0, 0));
assert_eq!(to_rc(5, 2, 3, &mut row, &mut col), OAKNODE_OK);
assert_eq!((row, col), (1, 2), "5 in 2x3 -> row 1 col 2");
assert_eq!(to_rc(2, 1, 2, &mut row, &mut col), OAKNODE_OK);
assert_eq!((row, col), (1, 0));
assert_eq!(to_rc(-1, 2, 3, &mut row, &mut col), OAKNODE_E_INVALID);
assert_eq!(to_rc(0, 0, 3, &mut row, &mut col), OAKNODE_E_INVALID);
assert_eq!(to_rc(0, 2, 0, &mut row, &mut col), OAKNODE_E_INVALID);
assert_eq!(
unsafe { oaknode_multicam_index_to_row_cols(0, 2, 3, std::ptr::null_mut(), &mut col) },
OAKNODE_E_INVALID
);
let to_i = |r: c_int, c: c_int, rr: c_int, cc: c_int| unsafe {
oaknode_multicam_rows_cols_to_index(r, c, rr, cc)
};
assert_eq!(to_i(1, 2, 2, 3), 5, "round-trip");
assert_eq!(to_i(0, 0, 1, 2), 0);
assert_eq!(to_i(-1, 0, 2, 3), OAKNODE_E_INVALID);
assert_eq!(to_i(0, -1, 2, 3), OAKNODE_E_INVALID);
assert_eq!(to_i(0, 0, 0, 3), OAKNODE_E_INVALID);
assert_eq!(to_i(2, 0, 2, 3), OAKNODE_E_INVALID, "row out of range");
assert_eq!(to_i(0, 3, 2, 3), OAKNODE_E_INVALID, "col out of range");
// Node-backed exports: a fake multicam node with 4 sources, current 2.
let mut core = oaknode::node::NodeCore::new();
let mut sources = oaknode::input::Input::new(
"sources_in",
oaknode::value::ValueType::None,
oaknode::value::NodeValue::None,
);
sources.flags = oaknode::input::flags::ARRAY | oaknode::input::flags::NOT_KEYFRAMABLE;
sources.array_size = 4;
core.add_input(sources);
core.add_input(oaknode::input::Input::new(
"current_in",
oaknode::value::ValueType::Combo,
oaknode::value::NodeValue::Combo(2),
));
core.set_standard_value("current_in", -1, oaknode::value::NodeValue::Combo(2));
let mut node = make_raw_node(core, Box::new(FakeMulticam));
let mut count = -1;
assert_eq!(
unsafe { oaknode_multicam_get_source_count(dup(&node), &mut count) },
OAKNODE_OK
);
assert_eq!(count, 4);
let mut src = -1;
assert_eq!(
unsafe { oaknode_multicam_get_current_source(dup(&node), &mut src) },
OAKNODE_OK
);
assert_eq!(src, 2);
// Non-multicam node / empty handle / null outs -> E_INVALID.
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut math = unsafe {
oaknode::ffi::factory::oaknode_factory_create_from_id(
cs("org.olivevideoeditor.Olive.math").as_ptr(),
)
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&math)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_multicam_get_source_count(dup(&math), &mut count) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_multicam_get_current_source(dup(&math), &mut src) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_multicam_get_source_count(CHandle::null(), &mut count) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_multicam_get_source_count(dup(&node), std::ptr::null_mut()) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_multicam_get_current_source(CHandle::null(), std::ptr::null_mut()) },
OAKNODE_E_INVALID
);
free(&mut node);
free(&mut math);
unsafe { oaknode_project_free(&mut p) };
}
/// Timeline/render bridge accessors: markers / work area / video frame
/// cache are addref'd stored handles (empty by default) with the
/// documented error paths; find_input_footage walks the upstream graph.
#[test]
fn bridge_handle_accessors() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
// A math node (not a viewer): markers/workarea empty, cache empty.
let mut math = unsafe {
oaknode::ffi::factory::oaknode_factory_create_from_id(
cs("org.olivevideoeditor.Olive.math").as_ptr(),
)
};
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&math)) },
OAKNODE_OK
);
let mut h = CHandle::null();
let hp = &mut h as *mut CHandle as *mut std::ffi::c_void;
assert_eq!(
unsafe { oaknode_node_get_markers(dup(&math), hp) },
OAKNODE_OK
);
assert!(h.ctx.is_null(), "no markers by default");
assert_eq!(
unsafe { oaknode_node_get_work_area(dup(&math), hp) },
OAKNODE_OK
);
assert!(h.ctx.is_null(), "no work area by default");
assert_eq!(
unsafe { oaknode_node_get_video_frame_cache(dup(&math), hp) },
OAKNODE_OK
);
assert!(h.ctx.is_null(), "no frame cache by default");
// Null args -> E_INVALID.
assert_eq!(
unsafe { oaknode_node_get_markers(CHandle::null(), hp) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_node_get_markers(dup(&math), std::ptr::null_mut()) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_node_get_work_area(CHandle::null(), hp) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_node_get_video_frame_cache(CHandle::null(), hp) },
OAKNODE_E_INVALID
);
// find_input_footage: no footage upstream -> empty + OK.
let mut out = CHandle::null();
assert_eq!(
unsafe { oaknode_node_find_input_footage(dup(&math), &mut out) },
OAKNODE_OK
);
assert!(out.ctx.is_null(), "no footage upstream");
assert_eq!(
unsafe { oaknode_node_find_input_footage(CHandle::null(), &mut out) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_node_find_input_footage(dup(&math), std::ptr::null_mut()) },
OAKNODE_E_INVALID
);
// A footage node feeding the math node's input: findable upstream.
let mut footage = unsafe { oaknode_footage_create(dup(&p), cs("/media/clip.mov").as_ptr()) };
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&footage)) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_node_connect(dup(&footage), dup(&math), cs("param_a_in").as_ptr()) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_node_find_input_footage(dup(&math), &mut out) },
OAKNODE_OK
);
assert!(!out.ctx.is_null(), "footage found upstream");
free(&mut out);
// The sequence (viewer) node also yields empty markers/workarea.
let mut seq = unsafe { oaknode_sequence_create() };
assert_eq!(
unsafe { oaknode_node_get_markers(dup(&seq), hp) },
OAKNODE_OK
);
assert!(h.ctx.is_null());
free(&mut seq);
free(&mut footage);
free(&mut math);
unsafe { oaknode_project_free(&mut p) };
}
/// Viewer params: set video/audio params from oakcommon/oakcore handles
/// (test-stub backed) with the documented degradation errors.
#[cfg(feature = "test-stubs")]
#[test]
fn viewer_stream_params() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut seq = unsafe { oaknode_sequence_create() };
// Build a video-params handle and push it into the viewer.
let mut vp =
oaknode::bridge::common::videoparams_init_basic(640, 480, 4, 4, 1, 1, 0, 1).unwrap();
assert_eq!(
oaknode::bridge::common::videoparams_set_frame_rate(dup(&vp), 24, 1).unwrap(),
0
);
assert_eq!(
unsafe {
oaknode_viewer_set_video_params(dup(&seq), &vp as *const _ as *const std::ffi::c_void)
},
OAKNODE_OK
);
// Read it back through the sequence getter and compare.
let mut vp2 = CHandle::null();
assert_eq!(
unsafe {
oaknode_sequence_get_video_params(
dup(&seq),
0,
&mut vp2 as *mut CHandle as *mut std::ffi::c_void,
)
},
OAKNODE_OK
);
assert!(!vp2.ctx.is_null());
assert_eq!(
oaknode::bridge::common::videoparams_get_width(dup(&vp2)).unwrap(),
640
);
assert_eq!(
oaknode::bridge::common::videoparams_get_height(dup(&vp2)).unwrap(),
480
);
assert_eq!(
oaknode::bridge::common::videoparams_get_frame_rate(dup(&vp2)).unwrap(),
(24, 1)
);
oaknode::bridge::common::videoparams_free(&mut vp2);
// Audio params via an oakcore handle.
let ap = oaknode::bridge::core::audioparams_create(48000, 0x3, 4).unwrap();
assert_eq!(
unsafe { oaknode_viewer_set_audio_params(dup(&seq), ap as *const std::ffi::c_void) },
OAKNODE_OK
);
let mut ap2: *mut std::ffi::c_void = std::ptr::null_mut();
assert_eq!(
unsafe { oaknode_sequence_get_audio_params(dup(&seq), 0, &mut ap2) },
OAKNODE_OK
);
assert!(!ap2.is_null());
assert_eq!(
oaknode::bridge::core::audioparams_sample_rate(ap2).unwrap(),
48000
);
assert_eq!(
oaknode::bridge::core::audioparams_channel_layout(ap2).unwrap(),
0x3
);
assert_eq!(oaknode::bridge::core::audioparams_format(ap2).unwrap(), 4);
oaknode::bridge::core::audioparams_free(ap2);
oaknode::bridge::core::audioparams_free(ap);
// Failure paths: null params / empty handle / non-viewer node.
assert_eq!(
unsafe { oaknode_viewer_set_video_params(dup(&seq), std::ptr::null()) },
OAKNODE_E_INVALID
);
let empty = CHandle::null();
assert_eq!(
unsafe {
oaknode_viewer_set_video_params(
dup(&seq),
&empty as *const _ as *const std::ffi::c_void,
)
},
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_viewer_set_audio_params(dup(&seq), std::ptr::null()) },
OAKNODE_E_INVALID
);
oaknode::bridge::common::videoparams_free(&mut vp);
free(&mut seq);
unsafe { oaknode_project_free(&mut p) };
}
/// Sequence/footage stream params: get/set video+audio round-trips
/// (test-stub backed) plus the error paths.
#[cfg(feature = "test-stubs")]
#[test]
fn stream_params_roundtrip() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
assert_eq!(unsafe { oaknode_project_initialize(dup(&p)) }, OAKNODE_OK);
let mut seq = unsafe { oaknode_sequence_create() };
assert_eq!(
unsafe { oaknode_sequence_set_default_parameters(dup(&seq)) },
OAKNODE_OK
);
let mut vc = 0;
assert_eq!(
unsafe { oaknode_sequence_get_video_stream_count(dup(&seq), &mut vc) },
OAKNODE_OK
);
assert_eq!(vc, 1, "default video stream");
// get_video_params(0) returns an owned handle.
let mut vp = CHandle::null();
assert_eq!(
unsafe {
oaknode_sequence_get_video_params(
dup(&seq),
0,
&mut vp as *mut CHandle as *mut std::ffi::c_void,
)
},
OAKNODE_OK
);
assert!(!vp.ctx.is_null());
assert_eq!(
oaknode::bridge::common::videoparams_get_width(dup(&vp)).unwrap(),
1920
);
// set_video_params(0) round-trips the same handle.
assert_eq!(
unsafe { oaknode_sequence_set_video_params(dup(&seq), 0, dup(&vp)) },
OAKNODE_OK
);
// Error paths.
assert_eq!(
unsafe {
oaknode_sequence_get_video_params(
dup(&seq),
7,
&mut vp as *mut CHandle as *mut std::ffi::c_void,
)
},
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe { oaknode_sequence_set_video_params(dup(&seq), 7, dup(&vp)) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe {
oaknode_sequence_get_video_params(
dup(&seq),
-1,
&mut vp as *mut CHandle as *mut std::ffi::c_void,
)
},
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_sequence_set_video_params(dup(&seq), 0, CHandle::null()) },
OAKNODE_E_INVALID
);
oaknode::bridge::common::videoparams_free(&mut vp);
// Audio round-trip.
let mut ac = 0;
assert_eq!(
unsafe { oaknode_sequence_get_audio_stream_count(dup(&seq), &mut ac) },
OAKNODE_OK
);
assert_eq!(ac, 1, "default audio stream");
let mut ap: *mut std::ffi::c_void = std::ptr::null_mut();
assert_eq!(
unsafe { oaknode_sequence_get_audio_params(dup(&seq), 0, &mut ap) },
OAKNODE_OK
);
assert!(!ap.is_null());
assert_eq!(
oaknode::bridge::core::audioparams_sample_rate(ap).unwrap(),
48000
);
assert_eq!(
unsafe { oaknode_sequence_set_audio_params(dup(&seq), 0, ap) },
OAKNODE_OK
);
assert_eq!(
unsafe { oaknode_sequence_get_audio_params(dup(&seq), 9, &mut ap) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe { oaknode_sequence_set_audio_params(dup(&seq), 9, ap) },
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe { oaknode_sequence_get_audio_params(dup(&seq), -1, &mut ap) },
OAKNODE_E_INVALID
);
assert_eq!(
unsafe { oaknode_sequence_set_audio_params(dup(&seq), 0, std::ptr::null()) },
OAKNODE_E_INVALID
);
oaknode::bridge::core::audioparams_free(ap);
// Footage: no video streams -> NOT_FOUND both ways; bad args.
let mut footage = unsafe { oaknode_footage_create(dup(&p), cs("/media/clip.mov").as_ptr()) };
assert_eq!(
unsafe { oaknode_project_add_node(dup(&p), dup(&footage)) },
OAKNODE_OK
);
let mut fvp = CHandle::null();
assert_eq!(
unsafe {
oaknode_footage_get_video_params(
dup(&footage),
0,
&mut fvp as *mut CHandle as *mut std::ffi::c_void,
)
},
OAKNODE_E_NOT_FOUND
);
assert_eq!(
unsafe {
oaknode_footage_set_video_params(
dup(&footage),
0,
&fvp as *const _ as *const std::ffi::c_void,
)
},
OAKNODE_E_INVALID,
"empty handle rejected"
);
assert_eq!(
unsafe {
oaknode_footage_get_video_params(
dup(&footage),
-1,
&mut fvp as *mut CHandle as *mut std::ffi::c_void,
)
},
OAKNODE_E_INVALID
);
free(&mut footage);
free(&mut seq);
unsafe { oaknode_project_free(&mut p) };
}
/// Colormanager compliant transform: a display transform whose view is
/// clamped to the active config's defaults (test-stub backed).
#[cfg(feature = "test-stubs")]
#[test]
fn colortransform_compliance() {
let _g = lock();
let mut p = unsafe { oaknode_project_init() };
let mut cm = unsafe { oaknode_colormanager_init(dup(&p)) };
// Config not loaded -> E_STATE.
let mut t =
oaknode::bridge::common::colortransform_init_display("sRGB", "Standard", "").unwrap();
let mut out = CHandle::null();
assert_eq!(
unsafe {
oaknode_colormanager_get_compliant_color_transform(dup(&cm), dup(&t), 0, &mut out)
},
OAKNODE_E_STATE
);
// Load the config, then a display transform with an unknown view gets
// clamped to the default view.
assert_eq!(
unsafe { oaknode_colormanager_initialize(dup(&cm)) },
OAKNODE_OK
);
assert_eq!(
unsafe {
oaknode_colormanager_get_compliant_color_transform(dup(&cm), dup(&t), 0, &mut out)
},
OAKNODE_OK
);
assert!(!out.ctx.is_null());
oaknode::bridge::common::colortransform_free(&mut out);
// An output transform with an unknown colorspace clamps to the
// default input space.
let mut t2 = oaknode::bridge::common::colortransform_init_output("no-such-space").unwrap();
assert_eq!(
unsafe {
oaknode_colormanager_get_compliant_color_transform(dup(&cm), dup(&t2), 0, &mut out)
},
OAKNODE_OK
);
assert!(!out.ctx.is_null());
oaknode::bridge::common::colortransform_free(&mut out);
// Failure: null out, empty manager.
assert_eq!(
unsafe {
oaknode_colormanager_get_compliant_color_transform(
dup(&cm),
dup(&t),
0,
std::ptr::null_mut(),
)
},
OAKNODE_E_INVALID
);
assert_eq!(
unsafe {
oaknode_colormanager_get_compliant_color_transform(
CHandle::null(),
dup(&t),
0,
&mut out,
)
},
OAKNODE_E_INVALID
);
oaknode::bridge::common::colortransform_free(&mut t);
oaknode::bridge::common::colortransform_free(&mut t2);
free(&mut cm);
unsafe { oaknode_project_free(&mut p) };
}