- 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)
252 lines
8.2 KiB
Rust
252 lines
8.2 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/>.
|
|
|
|
//! Edge-path coverage: error-code mapping, borrowed handles, refcount
|
|
//! symmetry, shell handles after `command_take`, `Default` impls, and the
|
|
//! `push_pre_executed` redo-tail/cap paths. Everything here goes through
|
|
//! the crate's public API or the C ABI.
|
|
|
|
use oakundo::error::{
|
|
Error, OAKUNDO_E_FAILED, OAKUNDO_E_INVALID, OAKUNDO_E_NOMEM, OAKUNDO_E_NOT_FOUND,
|
|
OAKUNDO_E_STATE,
|
|
};
|
|
use oakundo::ffi::command::{oakundo_command_init, oakundo_command_redo_now};
|
|
use oakundo::ffi::undostack::{
|
|
oakundo_undostack_index, oakundo_undostack_init, oakundo_undostack_push,
|
|
oakundo_undostack_push_pre_executed, oakundo_undostack_undo,
|
|
};
|
|
use oakundo::handle::{make_borrowed, make_owned};
|
|
use oakundo::undocommand::{MultiUndoCommand, OakUndoCommandVtable, UndoCommand};
|
|
use oakundo::undostack::{EmptyCommand, UndoStack, K_MAX_UNDO_COMMANDS};
|
|
|
|
/// Every `Error` variant maps to its documented public code.
|
|
#[test]
|
|
fn error_code_mapping_is_complete() {
|
|
assert_eq!(Error::Invalid.code(), OAKUNDO_E_INVALID);
|
|
assert_eq!(Error::State.code(), OAKUNDO_E_STATE);
|
|
assert_eq!(Error::Failed("ctx".to_string()).code(), OAKUNDO_E_FAILED);
|
|
assert_eq!(Error::NotFound.code(), OAKUNDO_E_NOT_FOUND);
|
|
assert_eq!(Error::NoMem.code(), OAKUNDO_E_NOMEM);
|
|
}
|
|
|
|
/// Borrowed handles: addref/release only touch the shell, never the
|
|
/// pointee; NULL ctx is a no-op for both.
|
|
#[test]
|
|
fn borrowed_handle_refcounting() {
|
|
let mut value: i32 = 7;
|
|
let h = unsafe { make_borrowed(&mut value as *mut i32) };
|
|
assert!(!h.is_null());
|
|
|
|
// NULL ctx is a no-op.
|
|
unsafe { h.addref.unwrap()(std::ptr::null_mut()) };
|
|
unsafe { h.release.unwrap()(std::ptr::null_mut()) };
|
|
|
|
// addref then two releases: the pointee survives (still readable).
|
|
unsafe { h.addref.unwrap()(h.ctx) };
|
|
unsafe { h.release.unwrap()(h.ctx) };
|
|
assert_eq!(value, 7);
|
|
unsafe { h.release.unwrap()(h.ctx) };
|
|
assert_eq!(value, 7);
|
|
}
|
|
|
|
/// Owned handles: addref requires a matching extra release; releasing to
|
|
/// zero destroys the box exactly once.
|
|
#[test]
|
|
fn owned_handle_refcounting() {
|
|
let h = make_owned(String::from("owned"));
|
|
assert!(!h.is_null());
|
|
unsafe { h.addref.unwrap()(h.ctx) };
|
|
unsafe { h.release.unwrap()(h.ctx) };
|
|
// Still alive (one reference left) and readable.
|
|
let view = unsafe { oakundo::handle::get::<String>(&h) };
|
|
assert_eq!(view.map(String::as_str), Some("owned"));
|
|
unsafe { h.release.unwrap()(h.ctx) };
|
|
}
|
|
|
|
/// `Default` impls mirror `new()`.
|
|
#[test]
|
|
fn default_impls_match_new() {
|
|
let _empty = EmptyCommand::default();
|
|
let stack = UndoStack::default();
|
|
assert_eq!(
|
|
stack.done_count(),
|
|
1,
|
|
"fresh stack holds the bottom command"
|
|
);
|
|
assert!(!stack.can_undo());
|
|
let multi = MultiUndoCommand::default();
|
|
assert_eq!(multi.child_count(), 0);
|
|
}
|
|
|
|
/// `set_prepared` is idempotent and `has_prepared` reflects it.
|
|
#[test]
|
|
fn prepared_flag_roundtrip() {
|
|
let vtable = OakUndoCommandVtable {
|
|
redo: None,
|
|
undo: None,
|
|
free_fn: None,
|
|
};
|
|
let mut cmd = UndoCommand::from_vtable(vtable, std::ptr::null_mut());
|
|
assert!(cmd.has_prepared());
|
|
cmd.set_prepared();
|
|
assert!(cmd.has_prepared());
|
|
}
|
|
|
|
/// A command handle whose value was taken by a stack push becomes a
|
|
/// non-owning shell: redo/undo on it are `E_INVALID`, and pushing it a
|
|
/// second time is `E_STATE`.
|
|
#[test]
|
|
fn taken_command_shell_is_inert() {
|
|
let vtable = OakUndoCommandVtable {
|
|
redo: None,
|
|
undo: None,
|
|
free_fn: None,
|
|
};
|
|
let mut stack = unsafe { oakundo_undostack_init() };
|
|
let cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) };
|
|
assert!(!cmd.ctx.is_null());
|
|
|
|
let name = c"once";
|
|
assert_eq!(
|
|
unsafe { oakundo_undostack_push(stack, cmd, name.as_ptr()) },
|
|
0
|
|
);
|
|
|
|
// The shell no longer holds a command value.
|
|
assert_eq!(unsafe { oakundo_command_redo_now(cmd) }, OAKUNDO_E_INVALID);
|
|
// Taking the same box twice is a state error.
|
|
assert_eq!(
|
|
unsafe { oakundo_undostack_push(stack, cmd, name.as_ptr()) },
|
|
OAKUNDO_E_STATE
|
|
);
|
|
|
|
let mut release = cmd;
|
|
unsafe {
|
|
oakundo::ffi::command::oakundo_command_free(&mut release);
|
|
oakundo::ffi::undostack::oakundo_undostack_free(&mut stack);
|
|
}
|
|
}
|
|
|
|
/// `push_pre_executed` also drops the redoable tail and evicts the oldest
|
|
/// row past the cap (mirrors `push`).
|
|
#[test]
|
|
fn push_pre_executed_clears_redo_tail_and_caps() {
|
|
let vtable = OakUndoCommandVtable {
|
|
redo: None,
|
|
undo: None,
|
|
free_fn: None,
|
|
};
|
|
let mut stack = unsafe { oakundo_undostack_init() };
|
|
let name = c"row";
|
|
|
|
// Push two, undo one, then push_pre_executed: redo tail is dropped.
|
|
for _ in 0..2 {
|
|
let cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) };
|
|
assert_eq!(
|
|
unsafe { oakundo_undostack_push(stack, cmd, name.as_ptr()) },
|
|
0
|
|
);
|
|
let mut shell = cmd;
|
|
unsafe { oakundo::ffi::command::oakundo_command_free(&mut shell) };
|
|
}
|
|
assert_eq!(unsafe { oakundo_undostack_undo(stack) }, 0);
|
|
let cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) };
|
|
assert_eq!(
|
|
unsafe { oakundo_undostack_push_pre_executed(stack, cmd, name.as_ptr()) },
|
|
0
|
|
);
|
|
let mut can_redo: i32 = 1;
|
|
assert_eq!(
|
|
unsafe { oakundo::ffi::undostack::oakundo_undostack_can_redo(stack, &mut can_redo) },
|
|
0
|
|
);
|
|
assert_eq!(can_redo, 0, "push_pre_executed drops the redoable tail");
|
|
|
|
// Fill past the cap with pre-executed commands: the oldest rows are
|
|
// evicted and the count stays at K_MAX_UNDO_COMMANDS.
|
|
for _ in 0..(K_MAX_UNDO_COMMANDS + 10) {
|
|
let cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) };
|
|
assert_eq!(
|
|
unsafe { oakundo_undostack_push_pre_executed(stack, cmd, name.as_ptr()) },
|
|
0
|
|
);
|
|
}
|
|
let mut index: i64 = 0;
|
|
assert_eq!(unsafe { oakundo_undostack_index(stack, &mut index) }, 0);
|
|
assert_eq!(
|
|
index, K_MAX_UNDO_COMMANDS as i64,
|
|
"pre-executed rows evict at the cap"
|
|
);
|
|
|
|
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(&mut stack) };
|
|
}
|
|
|
|
/// `make_owned` on a stack mutex is what `undostack_init` uses; the
|
|
/// `CHandle` accessors tolerate empty handles.
|
|
#[test]
|
|
fn empty_chandle_accessors() {
|
|
let h = oakundo::handle::CHandle::null();
|
|
assert!(h.is_null());
|
|
assert!(h.addref.is_none() && h.release.is_none());
|
|
assert_eq!(h.abi_version, 0);
|
|
let view = unsafe { oakundo::handle::get::<i32>(&h) };
|
|
assert!(view.is_none());
|
|
}
|
|
|
|
/// Command-box refcounting: addref/release tolerate NULL ctx, a bumped
|
|
/// refcount needs a matching release, and a taken multi shell reports
|
|
/// `E_INVALID` instead of dereferencing a null command pointer.
|
|
#[test]
|
|
fn command_box_refcounting_and_taken_multi_shell() {
|
|
let vtable = OakUndoCommandVtable {
|
|
redo: None,
|
|
undo: None,
|
|
free_fn: None,
|
|
};
|
|
|
|
let mut cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) };
|
|
assert!(!cmd.ctx.is_null());
|
|
|
|
// NULL ctx is a no-op for both refcount callbacks.
|
|
unsafe { cmd.addref.unwrap()(std::ptr::null_mut()) };
|
|
unsafe { cmd.release.unwrap()(std::ptr::null_mut()) };
|
|
|
|
// addref then two releases: destroyed exactly once at zero.
|
|
unsafe { cmd.addref.unwrap()(cmd.ctx) };
|
|
unsafe { cmd.release.unwrap()(cmd.ctx) };
|
|
unsafe { cmd.release.unwrap()(cmd.ctx) };
|
|
cmd.ctx = std::ptr::null_mut();
|
|
|
|
// A multi command pushed into a stack is taken; the remaining shell
|
|
// must fail cleanly on child access.
|
|
let mut stack = unsafe { oakundo_undostack_init() };
|
|
let mut multi = unsafe { oakundo::ffi::command::oakundo_command_init_multi() };
|
|
let name = c"m";
|
|
assert_eq!(
|
|
unsafe { oakundo_undostack_push(stack, multi, name.as_ptr()) },
|
|
0
|
|
);
|
|
let mut out: i32 = -1;
|
|
assert_eq!(
|
|
unsafe { oakundo::ffi::command::oakundo_command_multi_child_count(multi, &mut out) },
|
|
OAKUNDO_E_INVALID
|
|
);
|
|
unsafe {
|
|
oakundo::ffi::command::oakundo_command_free(&mut multi);
|
|
oakundo::ffi::undostack::oakundo_undostack_free(&mut stack);
|
|
}
|
|
}
|