// 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 . //! 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 Rust API (the handle-level functions sunk from the //! former C ABI layer). use oakundo::error::{ Error, OAKUNDO_E_FAILED, OAKUNDO_E_INVALID, OAKUNDO_E_NOMEM, OAKUNDO_E_NOT_FOUND, OAKUNDO_E_STATE, }; use oakundo::handle::{make_borrowed, make_owned}; use oakundo::undocommand::{ command_free, command_init, command_init_multi, command_multi_child_count, command_redo_now, MultiUndoCommand, OakUndoCommandVtable, UndoCommand, }; use oakundo::undostack::{ undostack_can_redo, undostack_free, undostack_index, undostack_init, undostack_push, undostack_push_pre_executed, undostack_undo, 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::(&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 = undostack_init(); let cmd = command_init(&vtable, std::ptr::null_mut()); assert!(!cmd.ctx.is_null()); let name = c"once"; assert_eq!(undostack_push(stack, cmd, name.as_ptr()), 0); // The shell no longer holds a command value. assert_eq!(command_redo_now(cmd), OAKUNDO_E_INVALID); // Taking the same box twice is a state error. assert_eq!(undostack_push(stack, cmd, name.as_ptr()), OAKUNDO_E_STATE); let mut release = cmd; command_free(&mut release); 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 = undostack_init(); let name = c"row"; // Push two, undo one, then push_pre_executed: redo tail is dropped. for _ in 0..2 { let cmd = command_init(&vtable, std::ptr::null_mut()); assert_eq!(undostack_push(stack, cmd, name.as_ptr()), 0); let mut shell = cmd; command_free(&mut shell); } assert_eq!(undostack_undo(stack), 0); let cmd = command_init(&vtable, std::ptr::null_mut()); assert_eq!(undostack_push_pre_executed(stack, cmd, name.as_ptr()), 0); let mut can_redo: i32 = 1; assert_eq!(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 = command_init(&vtable, std::ptr::null_mut()); assert_eq!(undostack_push_pre_executed(stack, cmd, name.as_ptr()), 0); } let mut index: i64 = 0; assert_eq!(undostack_index(stack, &mut index), 0); assert_eq!( index, K_MAX_UNDO_COMMANDS as i64, "pre-executed rows evict at the cap" ); 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::(&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 = 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 = undostack_init(); let mut multi = command_init_multi(); let name = c"m"; assert_eq!(undostack_push(stack, multi, name.as_ptr()), 0); let mut out: i32 = -1; assert_eq!(command_multi_child_count(multi, &mut out), OAKUNDO_E_INVALID); command_free(&mut multi); undostack_free(&mut stack); }