Files
oak-editor/crates/oakundo/tests/ffi_contract_test.rs
T
Mike-Solar ab1a2e9c7b refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are
gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/
oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain
Rust, CHandle marshalling shrinks to the oakengine boundary, and tests
call the Rust APIs directly (pure C-ABI wrapper tests removed where
the domain layer already covers the behavior).

exporter.h family implemented: oakengine_export_render (CLI contract),
oakengine_export_render_with_params (was a stub), last_error and
progress callback; synchronous path reuses task_create_export +
start_sync. Fixes on the way: oaktask video ticket self-deadlock,
audio params dropped on the export path, codec encoder AAC slicing and
H.264 time base. Real-mp4 tests cover both entry points, progress and
the illegal-argument matrix.

Also: oakstorage session maps null project handles to None (version-
info path), configstore test double literal 3.14 -> 3.15 (clippy PI
lint), oakaudio output callback scratch buffer + env-aware P1 test,
cli media round-trip test uses a generated 16-frame clip (no more
minute-long debug runs).
2026-08-16 00:33:45 +08:00

682 lines
23 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/>.
//! Contract tests for the handle-level command/stack API (the functions
//! sunk from the former `ffi.rs`). Each function gets at least one success
//! path and one failure path; complex multi-command and stack behavior is
//! exercised as a matrix. The expected semantics are pinned by the C++
//! module (`src/undo/src`, unchanged).
use std::cell::RefCell;
use std::ffi::{c_char, c_int, c_void, CString};
use oakundo::error::{OAKUNDO_E_INVALID, OAKUNDO_E_NOT_FOUND, OAKUNDO_OK};
use oakundo::handle::CHandle;
use oakundo::handle::OAKUNDO_ABI_VERSION;
use oakundo::undocommand::{
command_free, command_init, command_init_multi, command_multi_add_child, command_multi_child,
command_multi_child_count, command_redo_now, command_undo_now, OakUndoCommandVtable,
};
use oakundo::undostack::{
undostack_can_redo, undostack_can_undo, undostack_clear, undostack_command_is_done,
undostack_command_text, undostack_count, undostack_free, undostack_index, undostack_init,
undostack_jump, undostack_push, undostack_push_pre_executed, undostack_redo, undostack_undo,
};
/// Shared event recorder driven through vtable callbacks.
struct Trace {
events: RefCell<Vec<String>>,
}
/// Per-command callback payload: a name and a shared trace.
struct Probe {
name: &'static str,
trace: *const Trace,
}
unsafe extern "C" fn probe_redo(u: *mut c_void) {
let p = unsafe { &mut *(u as *mut Probe) };
let trace = unsafe { &*p.trace };
trace.events.borrow_mut().push(format!("redo:{}", p.name));
}
unsafe extern "C" fn probe_undo(u: *mut c_void) {
let p = unsafe { &mut *(u as *mut Probe) };
let trace = unsafe { &*p.trace };
trace.events.borrow_mut().push(format!("undo:{}", p.name));
}
unsafe extern "C" fn probe_free(u: *mut c_void) {
let p = unsafe { &mut *(u as *mut Probe) };
let trace = unsafe { &*p.trace };
trace.events.borrow_mut().push(format!("free:{}", p.name));
}
/// Snapshot of the recorded events, in order.
fn events(trace: &Trace) -> Vec<String> {
trace.events.borrow().clone()
}
/// A fresh trace plus three named probes (`a`, `b`, `c`) pointing at it.
fn setup() -> (Box<Trace>, Vec<Probe>) {
let trace = Box::new(Trace {
events: RefCell::new(Vec::new()),
});
let ptr = &*trace as *const Trace;
let mut probes = Vec::new();
for name in ["a", "b", "c"] {
probes.push(Probe { name, trace: ptr });
}
(trace, probes)
}
/// A vtable-backed command handle whose callbacks record into `probe`.
fn make_cmd(probe: *mut Probe) -> CHandle {
let vtable = OakUndoCommandVtable {
redo: Some(probe_redo),
undo: Some(probe_undo),
free_fn: Some(probe_free),
};
command_init(&vtable, probe as *mut c_void)
}
/// An empty (all-zero) command handle.
fn empty_cmd() -> CHandle {
CHandle {
ctx: std::ptr::null_mut(),
addref: None,
release: None,
abi_version: 0,
}
}
/// An empty (all-zero) stack handle.
fn empty_stack() -> CHandle {
CHandle {
ctx: std::ptr::null_mut(),
addref: None,
release: None,
abi_version: 0,
}
}
/// A fresh stack handle (refcount 1) for the calling test.
fn new_stack() -> CHandle {
undostack_init()
}
// ---------------------------------------------------------------------------
// Skeleton contract tests (ffi_contract_test.rs)
// ---------------------------------------------------------------------------
/// Command lifecycle: `init` returns a refcounted handle, `redo_now` marks
/// it done (a second `redo_now` is a no-op), `undo_now` un-done it, and
/// `free` runs `free_fn` once and clears `ctx`.
#[test]
fn command_lifecycle() {
let (trace, mut probes) = setup();
let mut cmd = make_cmd(&mut probes[0] as *mut Probe);
assert!(!cmd.ctx.is_null());
assert_eq!(cmd.abi_version, OAKUNDO_ABI_VERSION);
assert!(cmd.addref.is_some() && cmd.release.is_some());
assert_eq!(command_redo_now(cmd), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a"]);
// Idempotent redo.
assert_eq!(command_redo_now(cmd), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a"]);
assert_eq!(command_undo_now(cmd), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a", "undo:a"]);
// Idempotent undo.
assert_eq!(command_undo_now(cmd), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a", "undo:a"]);
// free destroys once: free_fn runs exactly once and ctx is cleared.
command_free(&mut cmd);
assert_eq!(events(&trace), vec!["redo:a", "undo:a", "free:a"]);
assert!(cmd.ctx.is_null());
}
/// Multi command: `add_child` → `child_count` reflects it; redo runs
/// children in order, undo in reverse order.
#[test]
fn multi_redo_undo_ordering() {
let (trace, mut probes) = setup();
let mut multi = command_init_multi();
// add children a, b, c.
let ca = make_cmd(&mut probes[0] as *mut Probe);
let cb = make_cmd(&mut probes[1] as *mut Probe);
let cc = make_cmd(&mut probes[2] as *mut Probe);
assert_eq!(command_multi_add_child(multi, ca), OAKUNDO_OK);
assert_eq!(command_multi_add_child(multi, cb), OAKUNDO_OK);
assert_eq!(command_multi_add_child(multi, cc), OAKUNDO_OK);
// child_count reflects three.
let mut count: c_int = 0;
assert_eq!(command_multi_child_count(multi, &mut count), OAKUNDO_OK);
assert_eq!(count, 3);
// redo fires in insertion order.
assert_eq!(command_redo_now(multi), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a", "redo:b", "redo:c"]);
// undo fires in reverse order.
assert_eq!(command_undo_now(multi), OAKUNDO_OK);
assert_eq!(
events(&trace),
vec!["redo:a", "redo:b", "redo:c", "undo:c", "undo:b", "undo:a"]
);
// Borrowed child handles are released harmlessly before the multi dies.
let mut child0 = empty_cmd();
assert_eq!(command_multi_child(multi, 0, &mut child0), OAKUNDO_OK);
assert!(!child0.ctx.is_null());
command_free(&mut child0);
// free the multi: children are freed (shell only) without double-free.
let mut multi_owned = multi;
command_free(&mut multi_owned);
assert_eq!(
events(&trace)
.iter()
.filter(|e| e.starts_with("free:"))
.count(),
3
);
}
/// Stack: fresh stack has one empty "New/Open Project" command, so
/// `can_undo` is 0 and `count` is 1; pushing redoable commands grows
/// `count` and makes `can_undo`/`can_redo` track the position.
#[test]
fn stack_push_undo_redo_queries() {
let (trace, mut probes) = setup();
let mut stack = new_stack();
let mut count: i64 = 0;
let mut value: c_int = 0;
assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK);
assert_eq!(count, 1);
assert_eq!(undostack_can_undo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 0);
assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 0);
// Push two commands.
let name_a = CString::new("A").unwrap();
let mut ca = make_cmd(&mut probes[0] as *mut Probe);
assert_eq!(undostack_push(stack, ca, name_a.as_ptr()), OAKUNDO_OK);
command_free(&mut ca); // non-owning shell now
let name_b = CString::new("B").unwrap();
let mut cb = make_cmd(&mut probes[1] as *mut Probe);
assert_eq!(undostack_push(stack, cb, name_b.as_ptr()), OAKUNDO_OK);
command_free(&mut cb);
assert_eq!(events(&trace), vec!["redo:a", "redo:b"]);
assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK);
assert_eq!(count, 3);
assert_eq!(undostack_can_undo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 1);
assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 0);
// Undo moves B into the redoable tail.
assert_eq!(undostack_undo(stack), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a", "redo:b", "undo:b"]);
assert_eq!(undostack_can_undo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 1);
assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 1);
// Redo restores.
assert_eq!(undostack_redo(stack), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a", "redo:b", "undo:b", "redo:b"]);
assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 0);
undostack_free(&mut stack);
}
/// `can_redo`/`index` after undo and redo; `jump(0)` clamps to the
/// bottom empty command without spinning; `jump` beyond the top is a no-op.
#[test]
fn stack_jump_clamps() {
let (trace, mut probes) = setup();
let mut stack = new_stack();
let na = CString::new("A").unwrap();
let nb = CString::new("B").unwrap();
let nc = CString::new("C").unwrap();
let mut ca = make_cmd(&mut probes[0] as *mut Probe);
undostack_push(stack, ca, na.as_ptr());
let mut cb = make_cmd(&mut probes[1] as *mut Probe);
undostack_push(stack, cb, nb.as_ptr());
let mut cc = make_cmd(&mut probes[2] as *mut Probe);
undostack_push(stack, cc, nc.as_ptr());
let mut index: i64 = 0;
assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK);
assert_eq!(index, 4);
// jump to the bottom (0): undo all three; negative clamps to 0 too.
assert_eq!(undostack_jump(stack, 0), OAKUNDO_OK);
assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK);
assert_eq!(index, 1);
assert_eq!(
events(&trace),
vec!["redo:a", "redo:b", "redo:c", "undo:c", "undo:b", "undo:a"]
);
// jump back up to 3: redo a and b (c was already undone to reach 1).
assert_eq!(undostack_jump(stack, 3), OAKUNDO_OK);
assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK);
assert_eq!(index, 3);
// jump beyond the top redoes up to the top (index 4), matching the C++
// `jump` (undone commands are redoable, so the second loop runs).
assert_eq!(undostack_jump(stack, 100), OAKUNDO_OK);
assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK);
assert_eq!(index, 4);
// Negative index clamps to the bottom.
assert_eq!(undostack_jump(stack, -5), OAKUNDO_OK);
assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK);
assert_eq!(index, 1);
undostack_free(&mut stack);
}
/// `push_pre_executed` records without redoing (stays undoable); empty
/// multi commands are discarded on push.
#[test]
fn stack_pre_executed_and_empty_multi() {
let (trace, mut probes) = setup();
let mut stack = new_stack();
// push_pre_executed: no redo callback.
let name = CString::new("Pre").unwrap();
let mut cp = make_cmd(&mut probes[0] as *mut Probe);
assert_eq!(undostack_push_pre_executed(stack, cp, name.as_ptr()), OAKUNDO_OK);
command_free(&mut cp);
assert_eq!(events(&trace), Vec::<String>::new());
// The pre-executed command is recorded as done (undoable).
let mut done: c_int = 0;
assert_eq!(undostack_command_is_done(stack, 1, &mut done), OAKUNDO_OK);
assert_eq!(done, 1);
let mut count: i64 = 0;
assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK);
assert_eq!(count, 2);
// Undoing the pre-executed command still runs its undo callback.
assert_eq!(undostack_undo(stack), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["undo:a"]);
// Empty multi command is discarded on push: count stays 2 (bottom
// "New/Open Project" + the undone pre-executed command), not 3.
let mut multi = command_init_multi();
let name2 = CString::new("Empty").unwrap();
assert_eq!(undostack_push(stack, multi, name2.as_ptr()), OAKUNDO_OK);
command_free(&mut multi);
assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK);
assert_eq!(count, 2, "empty multi is discarded on push");
undostack_free(&mut stack);
}
/// Every handle-returning export returns `ctx == NULL` on failure and a
/// valid handle (`abi_version` stamped) on success; `free(NULL)` /
/// `free(empty)` are no-ops across command and stack families.
#[test]
fn handle_and_free_contract() {
let (_trace, mut probes) = setup();
// init with a NULL vtable → empty handle.
let empty = command_init(std::ptr::null(), std::ptr::null_mut());
assert!(empty.ctx.is_null());
assert_eq!(empty.abi_version, 0);
assert!(empty.addref.is_none() && empty.release.is_none());
// init with a valid vtable → stamped handle.
let mut cmd = make_cmd(&mut probes[0] as *mut Probe);
assert!(!cmd.ctx.is_null());
assert_eq!(cmd.abi_version, OAKUNDO_ABI_VERSION);
// init_multi → stamped handle.
let mut multi = command_init_multi();
assert!(!multi.ctx.is_null());
assert_eq!(multi.abi_version, OAKUNDO_ABI_VERSION);
// init stack → stamped handle.
let mut stack = new_stack();
assert!(!stack.ctx.is_null());
assert_eq!(stack.abi_version, OAKUNDO_ABI_VERSION);
// free(NULL) is a no-op for both families.
command_free(std::ptr::null_mut());
undostack_free(std::ptr::null_mut());
// free(empty handle value) is a no-op.
let mut ecmd = empty_cmd();
command_free(&mut ecmd);
assert!(ecmd.ctx.is_null());
let mut estack = empty_stack();
undostack_free(&mut estack);
assert!(estack.ctx.is_null());
// free(valid) clears ctx.
command_free(&mut multi);
assert!(multi.ctx.is_null());
command_free(&mut cmd);
assert!(cmd.ctx.is_null());
undostack_free(&mut stack);
assert!(stack.ctx.is_null());
}
// ---------------------------------------------------------------------------
// Exhaustive per-export success/failure coverage
// ---------------------------------------------------------------------------
#[test]
fn command_redo_undo_now_null_is_invalid() {
assert_eq!(command_redo_now(empty_cmd()), OAKUNDO_E_INVALID);
assert_eq!(command_undo_now(empty_cmd()), OAKUNDO_E_INVALID);
}
#[test]
fn command_multi_add_child_errors() {
let (_trace, mut probes) = setup();
let mut multi = command_init_multi();
let mut child = make_cmd(&mut probes[0] as *mut Probe);
let mut vtable_cmd = make_cmd(&mut probes[1] as *mut Probe);
// multi is null → E_INVALID.
assert_eq!(command_multi_add_child(empty_cmd(), child), OAKUNDO_E_INVALID);
// child is null → E_INVALID (nothing taken).
assert_eq!(command_multi_add_child(multi, empty_cmd()), OAKUNDO_E_INVALID);
// target is a vtable command, not a multi → E_INVALID.
assert_eq!(command_multi_add_child(vtable_cmd, child), OAKUNDO_E_INVALID);
// The child handle still owns its value (never taken), so free it.
command_free(&mut child);
command_free(&mut vtable_cmd);
command_free(&mut multi);
}
#[test]
fn command_multi_child_count_errors() {
let (_trace, mut probes) = setup();
let mut multi = command_init_multi();
let mut vtable_cmd = make_cmd(&mut probes[0] as *mut Probe);
let mut out: c_int = -1;
// null out pointer → E_INVALID.
assert_eq!(command_multi_child_count(multi, std::ptr::null_mut()), OAKUNDO_E_INVALID);
// null multi → E_INVALID.
assert_eq!(command_multi_child_count(empty_cmd(), &mut out), OAKUNDO_E_INVALID);
// non-multi command → E_INVALID.
assert_eq!(command_multi_child_count(vtable_cmd, &mut out), OAKUNDO_E_INVALID);
command_free(&mut vtable_cmd);
command_free(&mut multi);
}
#[test]
fn command_multi_child_errors() {
let (_trace, mut probes) = setup();
let mut multi = command_init_multi();
let mut vtable_cmd = make_cmd(&mut probes[0] as *mut Probe);
let mut out = empty_cmd();
// null out pointer → E_INVALID.
assert_eq!(command_multi_child(multi, 0, std::ptr::null_mut()), OAKUNDO_E_INVALID);
// null multi → E_INVALID.
assert_eq!(command_multi_child(empty_cmd(), 0, &mut out), OAKUNDO_E_INVALID);
// non-multi command → E_INVALID.
assert_eq!(command_multi_child(vtable_cmd, 0, &mut out), OAKUNDO_E_INVALID);
// empty multi, negative index → E_NOT_FOUND.
assert_eq!(command_multi_child(multi, -1, &mut out), OAKUNDO_E_NOT_FOUND);
// empty multi, positive OOB → E_NOT_FOUND.
assert_eq!(command_multi_child(multi, 5, &mut out), OAKUNDO_E_NOT_FOUND);
command_free(&mut vtable_cmd);
command_free(&mut multi);
}
#[test]
fn command_multi_child_success_borrowed() {
let (trace, mut probes) = setup();
let mut multi = command_init_multi();
let mut ca = make_cmd(&mut probes[0] as *mut Probe);
command_multi_add_child(multi, ca);
let mut child = empty_cmd();
assert_eq!(command_multi_child(multi, 0, &mut child), OAKUNDO_OK);
assert!(!child.ctx.is_null());
// A borrowed child is not independently owned: freeing it frees only the
// shell and must not free the child the multi still owns.
command_free(&mut child);
assert!(child.ctx.is_null());
// Redo through the multi still works and no child was freed.
assert_eq!(command_redo_now(multi), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a"]);
command_free(&mut multi);
assert_eq!(events(&trace), vec!["redo:a", "free:a"]);
}
#[test]
fn command_free_fires_exactly_once() {
let (trace, mut probes) = setup();
let mut cmd = make_cmd(&mut probes[0] as *mut Probe);
// free → one free callback.
command_free(&mut cmd);
assert_eq!(events(&trace), vec!["free:a"]);
// free again on a cleared handle → no-op.
command_free(&mut cmd);
assert_eq!(events(&trace), vec!["free:a"]);
}
#[test]
fn undostack_free_null_and_empty() {
let mut stack = empty_stack();
undostack_free(std::ptr::null_mut());
undostack_free(&mut stack);
assert!(stack.ctx.is_null());
}
#[test]
fn undostack_push_errors() {
let (trace, mut probes) = setup();
let mut stack = new_stack();
let mut cmd = make_cmd(&mut probes[0] as *mut Probe);
let name = CString::new("A").unwrap();
// null command → E_INVALID; stack untouched, command still owned.
assert_eq!(undostack_push(stack, empty_cmd(), name.as_ptr()), OAKUNDO_E_INVALID);
// empty stack → E_INVALID; command NOT drained (still owns its value).
assert_eq!(undostack_push(empty_stack(), cmd, name.as_ptr()), OAKUNDO_E_INVALID);
assert_eq!(
events(&trace),
Vec::<String>::new(),
"no callbacks ran on failed push"
);
// NULL name is accepted as an empty label → OK.
assert_eq!(undostack_push(stack, cmd, std::ptr::null()), OAKUNDO_OK);
command_free(&mut cmd); // now a non-owning shell
undostack_free(&mut stack);
}
#[test]
fn undostack_push_pre_executed_errors() {
let (_trace, mut probes) = setup();
let mut stack = new_stack();
let mut cmd = make_cmd(&mut probes[0] as *mut Probe);
let name = CString::new("A").unwrap();
assert_eq!(
undostack_push_pre_executed(empty_stack(), cmd, name.as_ptr()),
OAKUNDO_E_INVALID
);
// command was not drained; still owned, so free it.
command_free(&mut cmd);
assert_eq!(
undostack_push_pre_executed(stack, empty_cmd(), name.as_ptr()),
OAKUNDO_E_INVALID
);
undostack_free(&mut stack);
}
#[test]
fn undostack_undo_redo_errors() {
let mut stack = new_stack();
assert_eq!(undostack_undo(empty_stack()), OAKUNDO_E_INVALID);
assert_eq!(undostack_redo(empty_stack()), OAKUNDO_E_INVALID);
// Valid stack, but nothing to undo: still OK (no-op).
assert_eq!(undostack_undo(stack), OAKUNDO_OK);
assert_eq!(undostack_redo(stack), OAKUNDO_OK);
undostack_free(&mut stack);
}
#[test]
fn undostack_jump_clear_errors() {
let mut stack = new_stack();
assert_eq!(undostack_jump(empty_stack(), 3), OAKUNDO_E_INVALID);
assert_eq!(undostack_clear(empty_stack()), OAKUNDO_E_INVALID);
// Clear on a valid stack resets to the empty bottom command.
let mut count: i64 = 0;
undostack_clear(stack);
assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK);
assert_eq!(count, 1);
undostack_free(&mut stack);
}
#[test]
fn undostack_can_undo_redo_errors() {
let mut stack = new_stack();
let mut value: c_int = 0;
assert_eq!(undostack_can_undo(empty_stack(), &mut value), OAKUNDO_E_INVALID);
assert_eq!(undostack_can_redo(empty_stack(), &mut value), OAKUNDO_E_INVALID);
assert_eq!(undostack_can_undo(stack, std::ptr::null_mut()), OAKUNDO_E_INVALID);
assert_eq!(undostack_can_redo(stack, std::ptr::null_mut()), OAKUNDO_E_INVALID);
undostack_free(&mut stack);
}
#[test]
fn undostack_count_index_errors() {
let mut stack = new_stack();
let mut out: i64 = 0;
assert_eq!(undostack_count(empty_stack(), &mut out), OAKUNDO_E_INVALID);
assert_eq!(undostack_index(empty_stack(), &mut out), OAKUNDO_E_INVALID);
assert_eq!(undostack_count(stack, std::ptr::null_mut()), OAKUNDO_E_INVALID);
assert_eq!(undostack_index(stack, std::ptr::null_mut()), OAKUNDO_E_INVALID);
undostack_free(&mut stack);
}
#[test]
fn undostack_command_text_two_stage_and_errors() {
let (_trace, mut probes) = setup();
let mut stack = new_stack();
let name = CString::new("MyAction").unwrap();
let mut cmd = make_cmd(&mut probes[0] as *mut Probe);
undostack_push(stack, cmd, name.as_ptr());
// Failure: empty stack → E_INVALID.
assert_eq!(undostack_command_text(empty_stack(), 1, std::ptr::null_mut(), 0), OAKUNDO_E_INVALID);
// Failure: OOB row (positive) → E_NOT_FOUND.
assert_eq!(undostack_command_text(stack, 5, std::ptr::null_mut(), 0), OAKUNDO_E_NOT_FOUND);
// Failure: negative row → E_NOT_FOUND.
assert_eq!(undostack_command_text(stack, -1, std::ptr::null_mut(), 0), OAKUNDO_E_NOT_FOUND);
// Stage one: null buffer returns the required size ("MyAction" + NUL).
let required = undostack_command_text(stack, 1, std::ptr::null_mut(), 0);
assert_eq!(required, "MyAction".len() as c_int + 1);
// Stage two: a buffer of that size is populated with a NUL-terminated
// string and the required size is returned again.
let mut buf = vec![0u8; required as usize];
let ret = undostack_command_text(stack, 1, buf.as_mut_ptr() as *mut c_char, required);
assert_eq!(ret, required);
let actual = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr() as *const c_char) };
assert_eq!(actual.to_bytes(), b"MyAction");
// A too-small buffer is safely truncated (NUL-terminated).
let mut small = vec![0xffu8; 3];
undostack_command_text(stack, 1, small.as_mut_ptr() as *mut c_char, 3);
assert_eq!(small, [b'M', b'y', 0]);
command_free(&mut cmd);
undostack_free(&mut stack);
}
#[test]
fn undostack_command_is_done_errors() {
let mut stack = new_stack();
let mut value: c_int = 0;
assert_eq!(undostack_command_is_done(empty_stack(), 0, &mut value), OAKUNDO_E_INVALID);
assert_eq!(undostack_command_is_done(stack, 0, std::ptr::null_mut()), OAKUNDO_E_INVALID);
assert_eq!(undostack_command_is_done(stack, 5, &mut value), OAKUNDO_E_NOT_FOUND);
assert_eq!(undostack_command_is_done(stack, -1, &mut value), OAKUNDO_E_NOT_FOUND);
undostack_free(&mut stack);
}
#[test]
fn undostack_push_drops_redoable_tail() {
let (trace, mut probes) = setup();
let mut stack = new_stack();
let na = CString::new("A").unwrap();
let nb = CString::new("B").unwrap();
let mut ca = make_cmd(&mut probes[0] as *mut Probe);
undostack_push(stack, ca, na.as_ptr());
let mut cb = make_cmd(&mut probes[1] as *mut Probe);
undostack_push(stack, cb, nb.as_ptr());
// Undo B, then push C → the redoable tail is dropped.
assert_eq!(undostack_undo(stack), OAKUNDO_OK);
let mut value: c_int = 0;
assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 1);
let nc = CString::new("C").unwrap();
let mut cc = make_cmd(&mut probes[2] as *mut Probe);
assert_eq!(undostack_push(stack, cc, nc.as_ptr()), OAKUNDO_OK);
assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 0, "pushing drops the redoable tail");
assert_eq!(
events(&trace),
vec!["redo:a", "redo:b", "undo:b", "free:b", "redo:c"]
);
command_free(&mut ca);
command_free(&mut cb);
command_free(&mut cc);
undostack_free(&mut stack);
}