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).
This commit is contained in:
2026-08-16 00:33:45 +08:00
parent 2248be8567
commit ab1a2e9c7b
293 changed files with 27826 additions and 90128 deletions
-427
View File
@@ -1,427 +0,0 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! C ABI export layer: implements `include/undo/*.h` verbatim.
//!
//! Organization: one submodule per public header. The authoritative
//! function list is the header itself; each submodule below carries a
//! complete inventory comment plus export stubs. Bodies only unwrap
//! handles, call the safe Rust domains, and map results through
//! [`crate::handle::guard*`]. `include/undo/error.h` exports macros
//! only, so it is folded into this module's preamble instead of getting
//! its own submodule.
//!
//! Handles cross the boundary by field copy between the public
//! [`OakUndoCommand`]/[`OakUndoStack`] structs and the crate-internal
//! [`CHandle`]. Stacks live behind a `Mutex` (shared state), commands in
//! a `CommandBox`.
use std::ffi::{c_char, c_int, c_void, CStr};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Mutex;
use crate::error::{Error, Result};
use crate::handle::{guard, guard_handle, guard_void, make_owned, CHandle};
use crate::undocommand::{
command_from_borrowed, command_from_owned, command_take, command_to_mut, command_to_ref,
OakUndoCommandVtable, UndoCommand,
};
use crate::undostack::UndoStack;
/// `include/undo/error.h` — macros only, no exported functions.
///
/// `OAKUNDO_OK` and the `OAKUNDO_E_*` codes are mirrored as
/// [`crate::error`] constants; `OAKUNDO_ABI_VERSION` lives in
/// [`crate::handle`].
/// `include/undo/undocommand.h` handle type — `OakUndoCommand`
/// (`{ctx, addref, release, abi_version}`). Single-lib unification:
/// aliases the shared [`CHandle`] (identical layout; the C ABI is
/// unchanged).
pub type OakUndoCommand = CHandle;
/// `include/undo/undostack.h` handle type — `OakUndoStack`. Single-lib
/// unification: aliases the shared [`CHandle`].
pub type OakUndoStack = CHandle;
/// Lock the stack behind `stack` and run `f` on it. `E_INVALID` for an
/// empty handle. A poisoned mutex is recovered (its inner value is
/// still valid).
fn with_stack<R>(stack: OakUndoStack, f: impl FnOnce(&mut UndoStack) -> Result<R>) -> Result<R> {
// Keep a local handle so `get`'s returned reference lives for the
// whole call (its lifetime ties to the `&CHandle` argument).
let ch = stack;
let m = unsafe { crate::handle::get::<Mutex<UndoStack>>(&ch) }.ok_or(Error::Invalid)?;
let mut guard = m.lock().unwrap_or_else(|e| e.into_inner());
f(&mut guard)
}
/// Read a NUL-terminated C string; `NULL` yields an empty string
/// (mirrors the C++ `name ? name : ""`).
fn read_name(name: *const c_char) -> String {
if name.is_null() {
String::new()
} else {
// SAFETY: `name` is a valid NUL-terminated string supplied by the
// caller, or NULL (already handled).
unsafe { CStr::from_ptr(name).to_string_lossy().into_owned() }
}
}
/// `include/undo/undocommand.h` exports (complete inventory):
/// command_init / command_init_multi / command_multi_add_child /
/// command_multi_child_count / command_multi_child / command_redo_now /
/// command_undo_now / command_free.
pub mod command {
use super::*;
/// `oakundo_command_init`: vtable-backed command, refcount 1.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_init(
vtable: *const OakUndoCommandVtable,
userdata: *mut c_void,
) -> OakUndoCommand {
guard_handle(|| unsafe {
if vtable.is_null() {
return Ok(CHandle::null());
}
let table = *vtable;
Ok(command_from_owned(UndoCommand::from_vtable(
table, userdata,
)))
})
}
/// `oakundo_command_init_multi`: empty multi command, refcount 1.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_init_multi() -> OakUndoCommand {
guard_handle(|| unsafe { Ok(command_from_owned(UndoCommand::multi())) })
}
/// `oakundo_command_multi_add_child` (stack takes one child ref).
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_multi_add_child(
multi: OakUndoCommand,
child: OakUndoCommand,
) -> c_int {
guard(|| unsafe {
let parent = command_to_mut(multi.ctx).ok_or(Error::Invalid)?;
if !parent.is_multi() {
return Err(Error::Invalid);
}
let child_cmd = command_take(child.ctx)?;
parent.multi_add_child(child_cmd);
Ok(())
})
}
/// `oakundo_command_multi_child_count`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_multi_child_count(
multi: OakUndoCommand,
out_count: *mut c_int,
) -> c_int {
guard(|| unsafe {
if out_count.is_null() {
return Err(Error::Invalid);
}
let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?;
if !parent.is_multi() {
return Err(Error::Invalid);
}
*out_count = parent.multi_child_count() as c_int;
Ok(())
})
}
/// `oakundo_command_multi_child` (returned handle carries own ref).
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_multi_child(
multi: OakUndoCommand,
index: c_int,
out_child: *mut OakUndoCommand,
) -> c_int {
guard(|| unsafe {
if out_child.is_null() {
return Err(Error::Invalid);
}
let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?;
if !parent.is_multi() {
return Err(Error::Invalid);
}
if index < 0 {
return Err(Error::NotFound);
}
let child = parent.multi_child(index as usize)?;
let ptr = child as *const UndoCommand as *mut UndoCommand;
*out_child = command_from_borrowed(ptr);
Ok(())
})
}
/// `oakundo_command_redo_now`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_redo_now(command: OakUndoCommand) -> c_int {
guard(|| unsafe {
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
c.redo_now();
Ok(())
})
}
/// `oakundo_command_undo_now`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_undo_now(command: OakUndoCommand) -> c_int {
guard(|| unsafe {
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
c.undo_now();
Ok(())
})
}
/// `oakundo_command_free`: NULL/empty no-op; clears `command->ctx`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_free(command: *mut OakUndoCommand) {
guard_void(|| unsafe {
if command.is_null() || (*command).ctx.is_null() {
return;
}
if let Some(release) = (*command).release {
release((*command).ctx);
}
(*command).ctx = std::ptr::null_mut();
})
}
}
/// `include/undo/undostack.h` exports (complete inventory):
/// undostack_init / undostack_free / undostack_push /
/// undostack_push_pre_executed / undostack_undo / undostack_redo /
/// undostack_jump / undostack_clear / undostack_can_undo /
/// undostack_can_redo / undostack_count / undostack_index /
/// undostack_command_text / undostack_command_is_done.
pub mod undostack {
use super::*;
/// `oakundo_undostack_init`: fresh stack, refcount 1.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_init() -> OakUndoStack {
guard_handle(|| Ok(make_owned(Mutex::new(UndoStack::new()))))
}
/// `oakundo_undostack_free`: NULL/empty no-op; clears `stack->ctx`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_free(stack: *mut OakUndoStack) {
guard_void(|| unsafe {
if stack.is_null() || (*stack).ctx.is_null() {
return;
}
if let Some(release) = (*stack).release {
release((*stack).ctx);
}
(*stack).ctx = std::ptr::null_mut();
})
}
/// `oakundo_undostack_push`: redo then record; drops redoable tail.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_push(
stack: OakUndoStack,
command: OakUndoCommand,
name: *const c_char,
) -> c_int {
guard(|| {
with_stack(stack, |s| unsafe {
let cmd = command_take(command.ctx)?;
let name = read_name(name);
s.push(cmd, &name);
Ok(())
})
})
}
/// `oakundo_undostack_push_pre_executed`: record without redoing.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_push_pre_executed(
stack: OakUndoStack,
command: OakUndoCommand,
name: *const c_char,
) -> c_int {
guard(|| {
with_stack(stack, |s| unsafe {
let cmd = command_take(command.ctx)?;
let name = read_name(name);
s.push_pre_executed(cmd, &name);
Ok(())
})
})
}
/// `oakundo_undostack_undo`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_undo(stack: OakUndoStack) -> c_int {
guard(|| with_stack(stack, |s| s.undo()))
}
/// `oakundo_undostack_redo`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_redo(stack: OakUndoStack) -> c_int {
guard(|| with_stack(stack, |s| s.redo()))
}
/// `oakundo_undostack_jump` (clamped to 0; `index` is i64).
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_jump(stack: OakUndoStack, index: i64) -> c_int {
guard(|| {
with_stack(stack, |s| {
s.jump(index);
Ok(())
})
})
}
/// `oakundo_undostack_clear`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_clear(stack: OakUndoStack) -> c_int {
guard(|| {
with_stack(stack, |s| {
s.clear();
Ok(())
})
})
}
/// `oakundo_undostack_can_undo`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_can_undo(
stack: OakUndoStack,
out_value: *mut c_int,
) -> c_int {
guard(|| unsafe {
if out_value.is_null() {
return Err(Error::Invalid);
}
with_stack(stack, |s| {
*out_value = if s.can_undo() { 1 } else { 0 };
Ok(())
})
})
}
/// `oakundo_undostack_can_redo`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_can_redo(
stack: OakUndoStack,
out_value: *mut c_int,
) -> c_int {
guard(|| unsafe {
if out_value.is_null() {
return Err(Error::Invalid);
}
with_stack(stack, |s| {
*out_value = if s.can_redo() { 1 } else { 0 };
Ok(())
})
})
}
/// `oakundo_undostack_count`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_count(
stack: OakUndoStack,
out_count: *mut i64,
) -> c_int {
guard(|| unsafe {
if out_count.is_null() {
return Err(Error::Invalid);
}
with_stack(stack, |s| {
*out_count = s.command_count();
Ok(())
})
})
}
/// `oakundo_undostack_index`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_index(
stack: OakUndoStack,
out_index: *mut i64,
) -> c_int {
guard(|| unsafe {
if out_index.is_null() {
return Err(Error::Invalid);
}
with_stack(stack, |s| {
*out_index = s.done_count();
Ok(())
})
})
}
/// `oakundo_undostack_command_text` (two-stage string getter).
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_command_text(
stack: OakUndoStack,
row: i64,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
let result = catch_unwind(AssertUnwindSafe(|| -> Result<i32> {
with_stack(stack, |s| {
if row < 0 || row >= s.command_count() {
return Err(Error::NotFound);
}
let name = s.command_name(row)?;
let required = (name.len() + 1) as i32;
if !buf.is_null() && buf_size > 0 {
let copy_len = name.len().min((buf_size as usize).saturating_sub(1));
let bytes = name.as_bytes();
// SAFETY: `buf` points to `buf_size` writable bytes and
// we write at most `copy_len` (+ one NUL) of them.
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
*buf.add(copy_len) = 0;
}
}
Ok(required)
})
}));
match result {
Ok(Ok(required)) => required,
Ok(Err(e)) => e.code(),
Err(_) => crate::error::OAKUNDO_E_FAILED,
}
}
/// `oakundo_undostack_command_is_done`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_command_is_done(
stack: OakUndoStack,
row: i64,
out_value: *mut c_int,
) -> c_int {
guard(|| unsafe {
if out_value.is_null() {
return Err(Error::Invalid);
}
with_stack(stack, |s| {
*out_value = if s.command_is_done(row)? { 1 } else { 0 };
Ok(())
})
})
}
}
-1
View File
@@ -31,7 +31,6 @@
#![warn(missing_docs)]
pub mod error;
pub mod ffi;
pub mod handle;
pub mod undocommand;
pub mod undostack;
+124 -8
View File
@@ -28,7 +28,7 @@
//!
//! ## Command boxes
//!
//! Commands cross the FFI boundary through a dedicated [`CommandBox`]
//! Commands cross crate boundaries through a dedicated [`CommandBox`]
//! (not the generic [`crate::handle::RefBox`]): it holds the raw
//! `*mut UndoCommand`, an `owns` flag (mirroring the C++
//! `OakUndoCommandBox`) and a refcount. An **owning** box owns and
@@ -36,12 +36,18 @@
//! reference into a `MultiUndoCommand` or an `UndoStack`) owns only its
//! shell. `take_command` moves the command value out of an owning box,
//! turning it into a non-owning shell (the C++ `mark_container_owned`).
//!
//! The handle-level functions below (`command_init` ... `command_free`)
//! were sunk from the former C ABI export layer: they keep the
//! `CHandle`-based signatures so the facade bridges and integration
//! tests can drive the command machinery directly. The `CHandle` layer
//! itself is removed in a later milestone.
use std::ffi::c_void;
use std::ffi::{c_int, c_void};
use std::sync::atomic::{AtomicU32, Ordering};
use crate::error::{Error, Result};
use crate::handle::{CHandle, OAKUNDO_ABI_VERSION};
use crate::handle::{guard, guard_handle, guard_void, CHandle, OAKUNDO_ABI_VERSION};
/// `oakundo_command_vtable` — the caller-defined redo/undo/free
/// callback table. Any entry may be `None`; a `None` redo/undo makes
@@ -348,7 +354,7 @@ pub(crate) struct CommandBox {
///
/// # Safety
/// The returned handle owns `cmd`; release it with `command_release`.
pub(crate) unsafe fn command_from_owned(cmd: UndoCommand) -> CHandle {
pub unsafe fn command_from_owned(cmd: UndoCommand) -> CHandle {
let boxed = Box::into_raw(Box::new(CommandBox {
command: Box::into_raw(Box::new(cmd)),
owns: true,
@@ -367,7 +373,7 @@ pub(crate) unsafe fn command_from_owned(cmd: UndoCommand) -> CHandle {
///
/// # Safety
/// `cmd` must outlive the returned handle.
pub(crate) unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle {
pub unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle {
let boxed = Box::into_raw(Box::new(CommandBox {
command: cmd,
owns: false,
@@ -385,7 +391,7 @@ pub(crate) unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle {
///
/// # Safety
/// `ctx` must come from a valid, live `CommandBox`.
pub(crate) unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCommand> {
pub unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCommand> {
unsafe {
if ctx.is_null() {
return None;
@@ -402,7 +408,7 @@ pub(crate) unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCom
///
/// # Safety
/// `ctx` must come from a valid, live `CommandBox`.
pub(crate) unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut UndoCommand> {
pub unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut UndoCommand> {
unsafe {
if ctx.is_null() {
return None;
@@ -420,7 +426,7 @@ pub(crate) unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut Und
///
/// # Safety
/// `ctx` must come from a valid, live `CommandBox`.
pub(crate) unsafe fn command_take(ctx: *mut c_void) -> Result<UndoCommand> {
pub unsafe fn command_take(ctx: *mut c_void) -> Result<UndoCommand> {
unsafe {
if ctx.is_null() {
return Err(Error::Invalid);
@@ -470,3 +476,113 @@ pub(crate) unsafe extern "C" fn command_release(ctx: *mut c_void) {
}
}
}
// ---------------------------------------------------------------------------
// Handle-level command API (sunk from the former C ABI export layer)
// ---------------------------------------------------------------------------
/// Create a vtable-backed command handle (refcount 1); a `NULL` vtable
/// yields an empty handle (`oakundo_command_init`).
pub fn command_init(vtable: *const OakUndoCommandVtable, userdata: *mut c_void) -> CHandle {
guard_handle(|| unsafe {
if vtable.is_null() {
return Ok(CHandle::null());
}
let table = *vtable;
Ok(command_from_owned(UndoCommand::from_vtable(table, userdata)))
})
}
/// Create an empty multi command handle (refcount 1)
/// (`oakundo_command_init_multi`).
pub fn command_init_multi() -> CHandle {
guard_handle(|| unsafe { Ok(command_from_owned(UndoCommand::multi())) })
}
/// Add `child` to a multi command handle (the stack/multi takes one
/// child reference); `E_INVALID` for an empty handle or a non-multi
/// parent (`oakundo_command_multi_add_child`).
pub fn command_multi_add_child(multi: CHandle, child: CHandle) -> c_int {
guard(|| unsafe {
let parent = command_to_mut(multi.ctx).ok_or(Error::Invalid)?;
if !parent.is_multi() {
return Err(Error::Invalid);
}
let child_cmd = command_take(child.ctx)?;
parent.multi_add_child(child_cmd);
Ok(())
})
}
/// Number of children of a multi command handle; `E_INVALID` for an
/// empty handle or a non-multi parent (`oakundo_command_multi_child_count`).
pub fn command_multi_child_count(multi: CHandle, out_count: *mut c_int) -> c_int {
guard(|| unsafe {
if out_count.is_null() {
return Err(Error::Invalid);
}
let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?;
if !parent.is_multi() {
return Err(Error::Invalid);
}
*out_count = parent.multi_child_count() as c_int;
Ok(())
})
}
/// Write a borrowed handle to the child at `index` of a multi command
/// (the returned handle carries its own shell ref); `E_NOT_FOUND` for
/// an out-of-range index (`oakundo_command_multi_child`).
pub fn command_multi_child(multi: CHandle, index: c_int, out_child: *mut CHandle) -> c_int {
guard(|| unsafe {
if out_child.is_null() {
return Err(Error::Invalid);
}
let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?;
if !parent.is_multi() {
return Err(Error::Invalid);
}
if index < 0 {
return Err(Error::NotFound);
}
let child = parent.multi_child(index as usize)?;
let ptr = child as *const UndoCommand as *mut UndoCommand;
*out_child = command_from_borrowed(ptr);
Ok(())
})
}
/// Execute a command handle's redo (no-op when already done)
/// (`oakundo_command_redo_now`).
pub fn command_redo_now(command: CHandle) -> c_int {
guard(|| unsafe {
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
c.redo_now();
Ok(())
})
}
/// Execute a command handle's undo (no-op when not done)
/// (`oakundo_command_undo_now`).
pub fn command_undo_now(command: CHandle) -> c_int {
guard(|| unsafe {
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
c.undo_now();
Ok(())
})
}
/// Release a command handle in place: run the release callback, then
/// clear `ctx`. `NULL` / empty handles are no-ops
/// (`oakundo_command_free`).
pub fn command_free(command: *mut CHandle) {
guard_void(|| unsafe {
if command.is_null() || (*command).ctx.is_null() {
return;
}
if let Some(release) = (*command).release {
release((*command).ctx);
}
(*command).ctx = std::ptr::null_mut();
})
}
+207 -2
View File
@@ -23,9 +23,13 @@
//! `can_undo` is false at the bottom (see `undostack.cpp::clear`).
use std::collections::VecDeque;
use std::ffi::{c_char, c_int, CStr};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Mutex;
use crate::error::{Error, Result};
use crate::undocommand::UndoCommand;
use crate::error::{Error, OAKUNDO_E_FAILED, Result};
use crate::handle::{get, guard, guard_handle, guard_void, make_owned, CHandle};
use crate::undocommand::{command_take, UndoCommand};
/// Maximum number of retained history rows (`k_max_undo_commands`).
pub const K_MAX_UNDO_COMMANDS: usize = 200;
@@ -218,3 +222,204 @@ impl Default for UndoStack {
Self::new()
}
}
// ---------------------------------------------------------------------------
// Handle-level stack API (sunk from the former C ABI export layer)
// ---------------------------------------------------------------------------
/// Read a NUL-terminated C string; `NULL` yields an empty string
/// (mirrors the C++ `name ? name : ""`).
fn read_name(name: *const c_char) -> String {
if name.is_null() {
String::new()
} else {
// SAFETY: `name` is a valid NUL-terminated string supplied by the
// caller, or NULL (already handled).
unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned()
}
}
/// Lock the stack behind `stack` and run `f` on it. `E_INVALID` for an
/// empty handle. A poisoned mutex is recovered (its inner value is still
/// valid).
pub fn with_stack<R>(stack: &CHandle, f: impl FnOnce(&mut UndoStack) -> Result<R>) -> Result<R> {
// SAFETY: the stack handle always boxes a `Mutex<UndoStack>` (created
// by `undostack_init`).
let m = unsafe { get::<Mutex<UndoStack>>(stack) }.ok_or(Error::Invalid)?;
let mut guard = m.lock().unwrap_or_else(|e| e.into_inner());
f(&mut guard)
}
/// Create a fresh stack handle (refcount 1) (`oakundo_undostack_init`).
pub fn undostack_init() -> CHandle {
guard_handle(|| Ok(make_owned(Mutex::new(UndoStack::new()))))
}
/// Release a stack handle in place; `NULL` / empty handles are no-ops
/// (`oakundo_undostack_free`).
pub fn undostack_free(stack: *mut CHandle) {
guard_void(|| unsafe {
if stack.is_null() || (*stack).ctx.is_null() {
return;
}
if let Some(release) = (*stack).release {
release((*stack).ctx);
}
(*stack).ctx = std::ptr::null_mut();
})
}
/// Push a command handle onto the stack (redo then record; the stack
/// takes ownership of the command value, leaving a non-owning shell)
/// (`oakundo_undostack_push`).
pub fn undostack_push(stack: CHandle, command: CHandle, name: *const c_char) -> c_int {
guard(|| {
with_stack(&stack, |s| unsafe {
let cmd = command_take(command.ctx)?;
let name = read_name(name);
s.push(cmd, &name);
Ok(())
})
})
}
/// Push an already-executed command handle (redo skipped)
/// (`oakundo_undostack_push_pre_executed`).
pub fn undostack_push_pre_executed(stack: CHandle, command: CHandle, name: *const c_char) -> c_int {
guard(|| {
with_stack(&stack, |s| unsafe {
let cmd = command_take(command.ctx)?;
let name = read_name(name);
s.push_pre_executed(cmd, &name);
Ok(())
})
})
}
/// Undo one command on the stack (`oakundo_undostack_undo`).
pub fn undostack_undo(stack: CHandle) -> c_int {
guard(|| with_stack(&stack, |s| s.undo()))
}
/// Redo one command on the stack (`oakundo_undostack_redo`).
pub fn undostack_redo(stack: CHandle) -> c_int {
guard(|| with_stack(&stack, |s| s.redo()))
}
/// Jump to a done-command index (clamped to 0; `index` is i64)
/// (`oakundo_undostack_jump`).
pub fn undostack_jump(stack: CHandle, index: i64) -> c_int {
guard(|| {
with_stack(&stack, |s| {
s.jump(index);
Ok(())
})
})
}
/// Clear the stack back to the empty bottom command
/// (`oakundo_undostack_clear`).
pub fn undostack_clear(stack: CHandle) -> c_int {
guard(|| {
with_stack(&stack, |s| {
s.clear();
Ok(())
})
})
}
/// Write whether an undo is possible (`oakundo_undostack_can_undo`).
pub fn undostack_can_undo(stack: CHandle, out_value: *mut c_int) -> c_int {
guard(|| unsafe {
if out_value.is_null() {
return Err(Error::Invalid);
}
with_stack(&stack, |s| {
*out_value = if s.can_undo() { 1 } else { 0 };
Ok(())
})
})
}
/// Write whether a redo is possible (`oakundo_undostack_can_redo`).
pub fn undostack_can_redo(stack: CHandle, out_value: *mut c_int) -> c_int {
guard(|| unsafe {
if out_value.is_null() {
return Err(Error::Invalid);
}
with_stack(&stack, |s| {
*out_value = if s.can_redo() { 1 } else { 0 };
Ok(())
})
})
}
/// Write the total history row count (`oakundo_undostack_count`).
pub fn undostack_count(stack: CHandle, out_count: *mut i64) -> c_int {
guard(|| unsafe {
if out_count.is_null() {
return Err(Error::Invalid);
}
with_stack(&stack, |s| {
*out_count = s.command_count();
Ok(())
})
})
}
/// Write the done-command count (`oakundo_undostack_index`).
pub fn undostack_index(stack: CHandle, out_index: *mut i64) -> c_int {
guard(|| unsafe {
if out_index.is_null() {
return Err(Error::Invalid);
}
with_stack(&stack, |s| {
*out_index = s.done_count();
Ok(())
})
})
}
/// Two-stage label getter for the row at `row`: returns the required
/// size (including NUL), or an error code; copies (truncating) when a
/// buffer is supplied (`oakundo_undostack_command_text`).
pub fn undostack_command_text(stack: CHandle, row: i64, buf: *mut c_char, buf_size: c_int) -> c_int {
let result = catch_unwind(AssertUnwindSafe(|| -> Result<i32> {
with_stack(&stack, |s| {
if row < 0 || row >= s.command_count() {
return Err(Error::NotFound);
}
let name = s.command_name(row)?;
let required = (name.len() + 1) as i32;
if !buf.is_null() && buf_size > 0 {
let copy_len = name.len().min((buf_size as usize).saturating_sub(1));
let bytes = name.as_bytes();
// SAFETY: `buf` points to `buf_size` writable bytes and we
// write at most `copy_len` (+ one NUL) of them.
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
*buf.add(copy_len) = 0;
}
}
Ok(required)
})
}));
match result {
Ok(Ok(required)) => required,
Ok(Err(e)) => e.code(),
Err(_) => OAKUNDO_E_FAILED,
}
}
/// Write whether the row at `row` is done (`oakundo_undostack_command_is_done`).
pub fn undostack_command_is_done(stack: CHandle, row: i64, out_value: *mut c_int) -> c_int {
guard(|| unsafe {
if out_value.is_null() {
return Err(Error::Invalid);
}
with_stack(&stack, |s| {
*out_value = if s.command_is_done(row)? { 1 } else { 0 };
Ok(())
})
})
}
+36 -62
View File
@@ -17,20 +17,22 @@
//! 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.
//! 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::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};
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]
@@ -115,29 +117,21 @@ fn taken_command_shell_is_inert() {
undo: None,
free_fn: None,
};
let mut stack = unsafe { oakundo_undostack_init() };
let cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) };
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!(
unsafe { oakundo_undostack_push(stack, cmd, name.as_ptr()) },
0
);
assert_eq!(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);
assert_eq!(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
);
assert_eq!(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);
}
command_free(&mut release);
undostack_free(&mut stack);
}
/// `push_pre_executed` also drops the redoable tail and evicts the oldest
@@ -149,49 +143,37 @@ fn push_pre_executed_clears_redo_tail_and_caps() {
undo: None,
free_fn: None,
};
let mut stack = unsafe { oakundo_undostack_init() };
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 = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) };
assert_eq!(
unsafe { oakundo_undostack_push(stack, cmd, name.as_ptr()) },
0
);
let cmd = command_init(&vtable, std::ptr::null_mut());
assert_eq!(undostack_push(stack, cmd, name.as_ptr()), 0);
let mut shell = cmd;
unsafe { oakundo::ffi::command::oakundo_command_free(&mut shell) };
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
);
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!(
unsafe { oakundo::ffi::undostack::oakundo_undostack_can_redo(stack, &mut can_redo) },
0
);
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 = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) };
assert_eq!(
unsafe { oakundo_undostack_push_pre_executed(stack, cmd, name.as_ptr()) },
0
);
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!(unsafe { oakundo_undostack_index(stack, &mut index) }, 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"
);
unsafe { oakundo::ffi::undostack::oakundo_undostack_free(&mut stack) };
undostack_free(&mut stack);
}
/// `make_owned` on a stack mutex is what `undostack_init` uses; the
@@ -217,7 +199,7 @@ fn command_box_refcounting_and_taken_multi_shell() {
free_fn: None,
};
let mut cmd = unsafe { oakundo_command_init(&vtable, std::ptr::null_mut()) };
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.
@@ -232,20 +214,12 @@ fn command_box_refcounting_and_taken_multi_shell() {
// 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 mut stack = undostack_init();
let mut multi = command_init_multi();
let name = c"m";
assert_eq!(
unsafe { oakundo_undostack_push(stack, multi, name.as_ptr()) },
0
);
assert_eq!(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);
}
assert_eq!(command_multi_child_count(multi, &mut out), OAKUNDO_E_INVALID);
command_free(&mut multi);
undostack_free(&mut stack);
}
+206 -410
View File
@@ -14,31 +14,27 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! C ABI contract tests (`ffi.rs`). Each exported 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).
//! 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::ffi::command::{
oakundo_command_free, oakundo_command_init, oakundo_command_init_multi,
oakundo_command_multi_add_child, oakundo_command_multi_child,
oakundo_command_multi_child_count, oakundo_command_redo_now, oakundo_command_undo_now,
};
use oakundo::ffi::undostack::{
oakundo_undostack_can_redo, oakundo_undostack_can_undo, oakundo_undostack_clear,
oakundo_undostack_command_is_done, oakundo_undostack_command_text, oakundo_undostack_count,
oakundo_undostack_free, oakundo_undostack_index, oakundo_undostack_init,
oakundo_undostack_jump, oakundo_undostack_push, oakundo_undostack_push_pre_executed,
oakundo_undostack_redo, oakundo_undostack_undo,
};
use oakundo::ffi::{OakUndoCommand, OakUndoStack};
use oakundo::handle::CHandle;
use oakundo::handle::OAKUNDO_ABI_VERSION;
use oakundo::undocommand::OakUndoCommandVtable;
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 {
@@ -88,17 +84,17 @@ fn setup() -> (Box<Trace>, Vec<Probe>) {
}
/// A vtable-backed command handle whose callbacks record into `probe`.
unsafe fn make_cmd(probe: *mut Probe) -> OakUndoCommand {
fn make_cmd(probe: *mut Probe) -> CHandle {
let vtable = OakUndoCommandVtable {
redo: Some(probe_redo),
undo: Some(probe_undo),
free_fn: Some(probe_free),
};
oakundo_command_init(&vtable, probe as *mut c_void)
command_init(&vtable, probe as *mut c_void)
}
/// An empty (all-zero) command handle.
fn empty_cmd() -> OakUndoCommand {
fn empty_cmd() -> CHandle {
CHandle {
ctx: std::ptr::null_mut(),
addref: None,
@@ -108,7 +104,7 @@ fn empty_cmd() -> OakUndoCommand {
}
/// An empty (all-zero) stack handle.
fn empty_stack() -> OakUndoStack {
fn empty_stack() -> CHandle {
CHandle {
ctx: std::ptr::null_mut(),
addref: None,
@@ -118,8 +114,8 @@ fn empty_stack() -> OakUndoStack {
}
/// A fresh stack handle (refcount 1) for the calling test.
unsafe fn new_stack() -> OakUndoStack {
oakundo_undostack_init()
fn new_stack() -> CHandle {
undostack_init()
}
// ---------------------------------------------------------------------------
@@ -132,28 +128,28 @@ unsafe fn new_stack() -> OakUndoStack {
#[test]
fn command_lifecycle() {
let (trace, mut probes) = setup();
let mut cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
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!(unsafe { oakundo_command_redo_now(cmd) }, OAKUNDO_OK);
assert_eq!(command_redo_now(cmd), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a"]);
// Idempotent redo.
assert_eq!(unsafe { oakundo_command_redo_now(cmd) }, OAKUNDO_OK);
assert_eq!(command_redo_now(cmd), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a"]);
assert_eq!(unsafe { oakundo_command_undo_now(cmd) }, OAKUNDO_OK);
assert_eq!(command_undo_now(cmd), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a", "undo:a"]);
// Idempotent undo.
assert_eq!(unsafe { oakundo_command_undo_now(cmd) }, OAKUNDO_OK);
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.
unsafe { oakundo_command_free(&mut cmd) };
command_free(&mut cmd);
assert_eq!(events(&trace), vec!["redo:a", "undo:a", "free:a"]);
assert!(cmd.ctx.is_null());
}
@@ -163,39 +159,27 @@ fn command_lifecycle() {
#[test]
fn multi_redo_undo_ordering() {
let (trace, mut probes) = setup();
let mut multi = unsafe { oakundo_command_init_multi() };
let mut multi = command_init_multi();
// add children a, b, c.
let ca = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
let cb = unsafe { make_cmd(&mut probes[1] as *mut Probe) };
let cc = unsafe { make_cmd(&mut probes[2] as *mut Probe) };
assert_eq!(
unsafe { oakundo_command_multi_add_child(multi, ca) },
OAKUNDO_OK
);
assert_eq!(
unsafe { oakundo_command_multi_add_child(multi, cb) },
OAKUNDO_OK
);
assert_eq!(
unsafe { oakundo_command_multi_add_child(multi, cc) },
OAKUNDO_OK
);
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!(
unsafe { oakundo_command_multi_child_count(multi, &mut count) },
OAKUNDO_OK
);
assert_eq!(command_multi_child_count(multi, &mut count), OAKUNDO_OK);
assert_eq!(count, 3);
// redo fires in insertion order.
assert_eq!(unsafe { oakundo_command_redo_now(multi) }, OAKUNDO_OK);
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!(unsafe { oakundo_command_undo_now(multi) }, OAKUNDO_OK);
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"]
@@ -203,16 +187,13 @@ fn multi_redo_undo_ordering() {
// Borrowed child handles are released harmlessly before the multi dies.
let mut child0 = empty_cmd();
assert_eq!(
unsafe { oakundo_command_multi_child(multi, 0, &mut child0) },
OAKUNDO_OK
);
assert_eq!(command_multi_child(multi, 0, &mut child0), OAKUNDO_OK);
assert!(!child0.ctx.is_null());
unsafe { oakundo_command_free(&mut child0) };
command_free(&mut child0);
// free the multi: children are freed (shell only) without double-free.
let mut multi_owned = multi;
unsafe { oakundo_command_free(&mut multi_owned) };
command_free(&mut multi_owned);
assert_eq!(
events(&trace)
.iter()
@@ -228,83 +209,50 @@ fn multi_redo_undo_ordering() {
#[test]
fn stack_push_undo_redo_queries() {
let (trace, mut probes) = setup();
let mut stack = unsafe { new_stack() };
let mut stack = new_stack();
let mut count: i64 = 0;
let mut value: c_int = 0;
assert_eq!(
unsafe { oakundo_undostack_count(stack, &mut count) },
OAKUNDO_OK
);
assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK);
assert_eq!(count, 1);
assert_eq!(
unsafe { oakundo_undostack_can_undo(stack, &mut value) },
OAKUNDO_OK
);
assert_eq!(undostack_can_undo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 0);
assert_eq!(
unsafe { oakundo_undostack_can_redo(stack, &mut value) },
OAKUNDO_OK
);
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 = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
assert_eq!(
unsafe { oakundo_undostack_push(stack, ca, name_a.as_ptr()) },
OAKUNDO_OK
);
unsafe { oakundo_command_free(&mut ca) }; // non-owning shell now
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 = unsafe { make_cmd(&mut probes[1] as *mut Probe) };
assert_eq!(
unsafe { oakundo_undostack_push(stack, cb, name_b.as_ptr()) },
OAKUNDO_OK
);
unsafe { oakundo_command_free(&mut cb) };
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!(
unsafe { oakundo_undostack_count(stack, &mut count) },
OAKUNDO_OK
);
assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK);
assert_eq!(count, 3);
assert_eq!(
unsafe { oakundo_undostack_can_undo(stack, &mut value) },
OAKUNDO_OK
);
assert_eq!(undostack_can_undo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 1);
assert_eq!(
unsafe { oakundo_undostack_can_redo(stack, &mut value) },
OAKUNDO_OK
);
assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 0);
// Undo moves B into the redoable tail.
assert_eq!(unsafe { oakundo_undostack_undo(stack) }, OAKUNDO_OK);
assert_eq!(undostack_undo(stack), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a", "redo:b", "undo:b"]);
assert_eq!(
unsafe { oakundo_undostack_can_undo(stack, &mut value) },
OAKUNDO_OK
);
assert_eq!(undostack_can_undo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 1);
assert_eq!(
unsafe { oakundo_undostack_can_redo(stack, &mut value) },
OAKUNDO_OK
);
assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 1);
// Redo restores.
assert_eq!(unsafe { oakundo_undostack_redo(stack) }, OAKUNDO_OK);
assert_eq!(undostack_redo(stack), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a", "redo:b", "undo:b", "redo:b"]);
assert_eq!(
unsafe { oakundo_undostack_can_redo(stack, &mut value) },
OAKUNDO_OK
);
assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 0);
unsafe { oakundo_undostack_free(&mut stack) };
undostack_free(&mut stack);
}
/// `can_redo`/`index` after undo and redo; `jump(0)` clamps to the
@@ -312,31 +260,25 @@ fn stack_push_undo_redo_queries() {
#[test]
fn stack_jump_clamps() {
let (trace, mut probes) = setup();
let mut stack = unsafe { new_stack() };
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 = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
unsafe { oakundo_undostack_push(stack, ca, na.as_ptr()) };
let mut cb = unsafe { make_cmd(&mut probes[1] as *mut Probe) };
unsafe { oakundo_undostack_push(stack, cb, nb.as_ptr()) };
let mut cc = unsafe { make_cmd(&mut probes[2] as *mut Probe) };
unsafe { oakundo_undostack_push(stack, cc, nc.as_ptr()) };
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!(
unsafe { oakundo_undostack_index(stack, &mut index) },
OAKUNDO_OK
);
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!(unsafe { oakundo_undostack_jump(stack, 0) }, OAKUNDO_OK);
assert_eq!(
unsafe { oakundo_undostack_index(stack, &mut index) },
OAKUNDO_OK
);
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),
@@ -344,31 +286,22 @@ fn stack_jump_clamps() {
);
// jump back up to 3: redo a and b (c was already undone to reach 1).
assert_eq!(unsafe { oakundo_undostack_jump(stack, 3) }, OAKUNDO_OK);
assert_eq!(
unsafe { oakundo_undostack_index(stack, &mut index) },
OAKUNDO_OK
);
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!(unsafe { oakundo_undostack_jump(stack, 100) }, OAKUNDO_OK);
assert_eq!(
unsafe { oakundo_undostack_index(stack, &mut index) },
OAKUNDO_OK
);
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!(unsafe { oakundo_undostack_jump(stack, -5) }, OAKUNDO_OK);
assert_eq!(
unsafe { oakundo_undostack_index(stack, &mut index) },
OAKUNDO_OK
);
assert_eq!(undostack_jump(stack, -5), OAKUNDO_OK);
assert_eq!(undostack_index(stack, &mut index), OAKUNDO_OK);
assert_eq!(index, 1);
unsafe { oakundo_undostack_free(&mut stack) };
undostack_free(&mut stack);
}
/// `push_pre_executed` records without redoing (stays undoable); empty
@@ -376,53 +309,38 @@ fn stack_jump_clamps() {
#[test]
fn stack_pre_executed_and_empty_multi() {
let (trace, mut probes) = setup();
let mut stack = unsafe { new_stack() };
let mut stack = new_stack();
// push_pre_executed: no redo callback.
let name = CString::new("Pre").unwrap();
let mut cp = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
assert_eq!(
unsafe { oakundo_undostack_push_pre_executed(stack, cp, name.as_ptr()) },
OAKUNDO_OK
);
unsafe { oakundo_command_free(&mut cp) };
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!(
unsafe { oakundo_undostack_command_is_done(stack, 1, &mut done) },
OAKUNDO_OK
);
assert_eq!(undostack_command_is_done(stack, 1, &mut done), OAKUNDO_OK);
assert_eq!(done, 1);
let mut count: i64 = 0;
assert_eq!(
unsafe { oakundo_undostack_count(stack, &mut count) },
OAKUNDO_OK
);
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!(unsafe { oakundo_undostack_undo(stack) }, OAKUNDO_OK);
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 = unsafe { oakundo_command_init_multi() };
let mut multi = command_init_multi();
let name2 = CString::new("Empty").unwrap();
assert_eq!(
unsafe { oakundo_undostack_push(stack, multi, name2.as_ptr()) },
OAKUNDO_OK
);
unsafe { oakundo_command_free(&mut multi) };
assert_eq!(
unsafe { oakundo_undostack_count(stack, &mut count) },
OAKUNDO_OK
);
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");
unsafe { oakundo_undostack_free(&mut stack) };
undostack_free(&mut stack);
}
/// Every handle-returning export returns `ctx == NULL` on failure and a
@@ -433,44 +351,44 @@ fn handle_and_free_contract() {
let (_trace, mut probes) = setup();
// init with a NULL vtable → empty handle.
let empty = unsafe { oakundo_command_init(std::ptr::null(), std::ptr::null_mut()) };
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 = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
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 = unsafe { oakundo_command_init_multi() };
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 = unsafe { new_stack() };
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.
unsafe { oakundo_command_free(std::ptr::null_mut()) };
unsafe { oakundo_undostack_free(std::ptr::null_mut()) };
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();
unsafe { oakundo_command_free(&mut ecmd) };
command_free(&mut ecmd);
assert!(ecmd.ctx.is_null());
let mut estack = empty_stack();
unsafe { oakundo_undostack_free(&mut estack) };
undostack_free(&mut estack);
assert!(estack.ctx.is_null());
// free(valid) clears ctx.
unsafe { oakundo_command_free(&mut multi) };
command_free(&mut multi);
assert!(multi.ctx.is_null());
unsafe { oakundo_command_free(&mut cmd) };
command_free(&mut cmd);
assert!(cmd.ctx.is_null());
unsafe { oakundo_undostack_free(&mut stack) };
undostack_free(&mut stack);
assert!(stack.ctx.is_null());
}
@@ -480,174 +398,126 @@ fn handle_and_free_contract() {
#[test]
fn command_redo_undo_now_null_is_invalid() {
assert_eq!(
unsafe { oakundo_command_redo_now(empty_cmd()) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_command_undo_now(empty_cmd()) },
OAKUNDO_E_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 = unsafe { oakundo_command_init_multi() };
let mut child = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
let mut vtable_cmd = unsafe { make_cmd(&mut probes[1] as *mut Probe) };
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!(
unsafe { oakundo_command_multi_add_child(empty_cmd(), child) },
OAKUNDO_E_INVALID
);
assert_eq!(command_multi_add_child(empty_cmd(), child), OAKUNDO_E_INVALID);
// child is null → E_INVALID (nothing taken).
assert_eq!(
unsafe { oakundo_command_multi_add_child(multi, empty_cmd()) },
OAKUNDO_E_INVALID
);
assert_eq!(command_multi_add_child(multi, empty_cmd()), OAKUNDO_E_INVALID);
// target is a vtable command, not a multi → E_INVALID.
assert_eq!(
unsafe { oakundo_command_multi_add_child(vtable_cmd, child) },
OAKUNDO_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.
unsafe { oakundo_command_free(&mut child) };
unsafe { oakundo_command_free(&mut vtable_cmd) };
unsafe { oakundo_command_free(&mut multi) };
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 = unsafe { oakundo_command_init_multi() };
let mut vtable_cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
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!(
unsafe { oakundo_command_multi_child_count(multi, std::ptr::null_mut()) },
OAKUNDO_E_INVALID
);
assert_eq!(command_multi_child_count(multi, std::ptr::null_mut()), OAKUNDO_E_INVALID);
// null multi → E_INVALID.
assert_eq!(
unsafe { oakundo_command_multi_child_count(empty_cmd(), &mut out) },
OAKUNDO_E_INVALID
);
assert_eq!(command_multi_child_count(empty_cmd(), &mut out), OAKUNDO_E_INVALID);
// non-multi command → E_INVALID.
assert_eq!(
unsafe { oakundo_command_multi_child_count(vtable_cmd, &mut out) },
OAKUNDO_E_INVALID
);
assert_eq!(command_multi_child_count(vtable_cmd, &mut out), OAKUNDO_E_INVALID);
unsafe { oakundo_command_free(&mut vtable_cmd) };
unsafe { oakundo_command_free(&mut multi) };
command_free(&mut vtable_cmd);
command_free(&mut multi);
}
#[test]
fn command_multi_child_errors() {
let (_trace, mut probes) = setup();
let mut multi = unsafe { oakundo_command_init_multi() };
let mut vtable_cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
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!(
unsafe { oakundo_command_multi_child(multi, 0, std::ptr::null_mut()) },
OAKUNDO_E_INVALID
);
assert_eq!(command_multi_child(multi, 0, std::ptr::null_mut()), OAKUNDO_E_INVALID);
// null multi → E_INVALID.
assert_eq!(
unsafe { oakundo_command_multi_child(empty_cmd(), 0, &mut out) },
OAKUNDO_E_INVALID
);
assert_eq!(command_multi_child(empty_cmd(), 0, &mut out), OAKUNDO_E_INVALID);
// non-multi command → E_INVALID.
assert_eq!(
unsafe { oakundo_command_multi_child(vtable_cmd, 0, &mut out) },
OAKUNDO_E_INVALID
);
assert_eq!(command_multi_child(vtable_cmd, 0, &mut out), OAKUNDO_E_INVALID);
// empty multi, negative index → E_NOT_FOUND.
assert_eq!(
unsafe { oakundo_command_multi_child(multi, -1, &mut out) },
OAKUNDO_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!(
unsafe { oakundo_command_multi_child(multi, 5, &mut out) },
OAKUNDO_E_NOT_FOUND
);
assert_eq!(command_multi_child(multi, 5, &mut out), OAKUNDO_E_NOT_FOUND);
unsafe { oakundo_command_free(&mut vtable_cmd) };
unsafe { oakundo_command_free(&mut multi) };
command_free(&mut vtable_cmd);
command_free(&mut multi);
}
#[test]
fn command_multi_child_success_borrowed() {
let (trace, mut probes) = setup();
let mut multi = unsafe { oakundo_command_init_multi() };
let mut ca = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
unsafe { oakundo_command_multi_add_child(multi, ca) };
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!(
unsafe { oakundo_command_multi_child(multi, 0, &mut child) },
OAKUNDO_OK
);
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.
unsafe { oakundo_command_free(&mut child) };
command_free(&mut child);
assert!(child.ctx.is_null());
// Redo through the multi still works and no child was freed.
assert_eq!(unsafe { oakundo_command_redo_now(multi) }, OAKUNDO_OK);
assert_eq!(command_redo_now(multi), OAKUNDO_OK);
assert_eq!(events(&trace), vec!["redo:a"]);
unsafe { oakundo_command_free(&mut multi) };
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 = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
let mut cmd = make_cmd(&mut probes[0] as *mut Probe);
// free → one free callback.
unsafe { oakundo_command_free(&mut cmd) };
command_free(&mut cmd);
assert_eq!(events(&trace), vec!["free:a"]);
// free again on a cleared handle → no-op.
unsafe { oakundo_command_free(&mut cmd) };
command_free(&mut cmd);
assert_eq!(events(&trace), vec!["free:a"]);
}
#[test]
fn undostack_free_null_and_empty() {
let mut stack = empty_stack();
unsafe { oakundo_undostack_free(std::ptr::null_mut()) };
unsafe { oakundo_undostack_free(&mut 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 = unsafe { new_stack() };
let mut cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
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!(
unsafe { oakundo_undostack_push(stack, empty_cmd(), name.as_ptr()) },
OAKUNDO_E_INVALID
);
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!(
unsafe { oakundo_undostack_push(empty_stack(), cmd, name.as_ptr()) },
OAKUNDO_E_INVALID
);
assert_eq!(undostack_push(empty_stack(), cmd, name.as_ptr()), OAKUNDO_E_INVALID);
assert_eq!(
events(&trace),
Vec::<String>::new(),
@@ -655,231 +525,157 @@ fn undostack_push_errors() {
);
// NULL name is accepted as an empty label → OK.
assert_eq!(
unsafe { oakundo_undostack_push(stack, cmd, std::ptr::null()) },
OAKUNDO_OK
);
assert_eq!(undostack_push(stack, cmd, std::ptr::null()), OAKUNDO_OK);
unsafe { oakundo_command_free(&mut cmd) }; // now a non-owning shell
unsafe { oakundo_undostack_free(&mut stack) };
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 = unsafe { new_stack() };
let mut cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
let mut stack = new_stack();
let mut cmd = make_cmd(&mut probes[0] as *mut Probe);
let name = CString::new("A").unwrap();
assert_eq!(
unsafe { oakundo_undostack_push_pre_executed(empty_stack(), cmd, name.as_ptr()) },
undostack_push_pre_executed(empty_stack(), cmd, name.as_ptr()),
OAKUNDO_E_INVALID
);
// command was not drained; still owned, so free it.
unsafe { oakundo_command_free(&mut cmd) };
command_free(&mut cmd);
assert_eq!(
unsafe { oakundo_undostack_push_pre_executed(stack, empty_cmd(), name.as_ptr()) },
undostack_push_pre_executed(stack, empty_cmd(), name.as_ptr()),
OAKUNDO_E_INVALID
);
unsafe { oakundo_undostack_free(&mut stack) };
undostack_free(&mut stack);
}
#[test]
fn undostack_undo_redo_errors() {
let mut stack = unsafe { new_stack() };
assert_eq!(
unsafe { oakundo_undostack_undo(empty_stack()) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_undostack_redo(empty_stack()) },
OAKUNDO_E_INVALID
);
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!(unsafe { oakundo_undostack_undo(stack) }, OAKUNDO_OK);
assert_eq!(unsafe { oakundo_undostack_redo(stack) }, OAKUNDO_OK);
unsafe { oakundo_undostack_free(&mut stack) };
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 = unsafe { new_stack() };
assert_eq!(
unsafe { oakundo_undostack_jump(empty_stack(), 3) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_undostack_clear(empty_stack()) },
OAKUNDO_E_INVALID
);
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;
unsafe { oakundo_undostack_clear(stack) };
assert_eq!(
unsafe { oakundo_undostack_count(stack, &mut count) },
OAKUNDO_OK
);
undostack_clear(stack);
assert_eq!(undostack_count(stack, &mut count), OAKUNDO_OK);
assert_eq!(count, 1);
unsafe { oakundo_undostack_free(&mut stack) };
undostack_free(&mut stack);
}
#[test]
fn undostack_can_undo_redo_errors() {
let mut stack = unsafe { new_stack() };
let mut stack = new_stack();
let mut value: c_int = 0;
assert_eq!(
unsafe { oakundo_undostack_can_undo(empty_stack(), &mut value) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_undostack_can_redo(empty_stack(), &mut value) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_undostack_can_undo(stack, std::ptr::null_mut()) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_undostack_can_redo(stack, std::ptr::null_mut()) },
OAKUNDO_E_INVALID
);
unsafe { oakundo_undostack_free(&mut stack) };
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 = unsafe { new_stack() };
let mut stack = new_stack();
let mut out: i64 = 0;
assert_eq!(
unsafe { oakundo_undostack_count(empty_stack(), &mut out) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_undostack_index(empty_stack(), &mut out) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_undostack_count(stack, std::ptr::null_mut()) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_undostack_index(stack, std::ptr::null_mut()) },
OAKUNDO_E_INVALID
);
unsafe { oakundo_undostack_free(&mut stack) };
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 = unsafe { new_stack() };
let mut stack = new_stack();
let name = CString::new("MyAction").unwrap();
let mut cmd = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
unsafe { oakundo_undostack_push(stack, cmd, name.as_ptr()) };
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!(
unsafe { oakundo_undostack_command_text(empty_stack(), 1, std::ptr::null_mut(), 0) },
OAKUNDO_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!(
unsafe { oakundo_undostack_command_text(stack, 5, std::ptr::null_mut(), 0) },
OAKUNDO_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!(
unsafe { oakundo_undostack_command_text(stack, -1, std::ptr::null_mut(), 0) },
OAKUNDO_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 = unsafe { oakundo_undostack_command_text(stack, 1, std::ptr::null_mut(), 0) };
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 = unsafe {
oakundo_undostack_command_text(stack, 1, buf.as_mut_ptr() as *mut c_char, required)
};
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];
unsafe { oakundo_undostack_command_text(stack, 1, small.as_mut_ptr() as *mut c_char, 3) };
undostack_command_text(stack, 1, small.as_mut_ptr() as *mut c_char, 3);
assert_eq!(small, [b'M', b'y', 0]);
unsafe { oakundo_command_free(&mut cmd) };
unsafe { oakundo_undostack_free(&mut stack) };
command_free(&mut cmd);
undostack_free(&mut stack);
}
#[test]
fn undostack_command_is_done_errors() {
let mut stack = unsafe { new_stack() };
let mut stack = new_stack();
let mut value: c_int = 0;
assert_eq!(
unsafe { oakundo_undostack_command_is_done(empty_stack(), 0, &mut value) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_undostack_command_is_done(stack, 0, std::ptr::null_mut()) },
OAKUNDO_E_INVALID
);
assert_eq!(
unsafe { oakundo_undostack_command_is_done(stack, 5, &mut value) },
OAKUNDO_E_NOT_FOUND
);
assert_eq!(
unsafe { oakundo_undostack_command_is_done(stack, -1, &mut value) },
OAKUNDO_E_NOT_FOUND
);
unsafe { oakundo_undostack_free(&mut stack) };
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_via_ffi() {
fn undostack_push_drops_redoable_tail() {
let (trace, mut probes) = setup();
let mut stack = unsafe { new_stack() };
let mut stack = new_stack();
let na = CString::new("A").unwrap();
let nb = CString::new("B").unwrap();
let mut ca = unsafe { make_cmd(&mut probes[0] as *mut Probe) };
unsafe { oakundo_undostack_push(stack, ca, na.as_ptr()) };
let mut cb = unsafe { make_cmd(&mut probes[1] as *mut Probe) };
unsafe { oakundo_undostack_push(stack, cb, nb.as_ptr()) };
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!(unsafe { oakundo_undostack_undo(stack) }, OAKUNDO_OK);
assert_eq!(undostack_undo(stack), OAKUNDO_OK);
let mut value: c_int = 0;
assert_eq!(
unsafe { oakundo_undostack_can_redo(stack, &mut value) },
OAKUNDO_OK
);
assert_eq!(undostack_can_redo(stack, &mut value), OAKUNDO_OK);
assert_eq!(value, 1);
let nc = CString::new("C").unwrap();
let mut cc = unsafe { make_cmd(&mut probes[2] as *mut Probe) };
assert_eq!(
unsafe { oakundo_undostack_push(stack, cc, nc.as_ptr()) },
OAKUNDO_OK
);
let mut cc = make_cmd(&mut probes[2] as *mut Probe);
assert_eq!(undostack_push(stack, cc, nc.as_ptr()), OAKUNDO_OK);
assert_eq!(
unsafe { oakundo_undostack_can_redo(stack, &mut value) },
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"]
);
unsafe { oakundo_command_free(&mut ca) };
unsafe { oakundo_command_free(&mut cb) };
unsafe { oakundo_command_free(&mut cc) };
unsafe { oakundo_undostack_free(&mut stack) };
command_free(&mut ca);
command_free(&mut cb);
command_free(&mut cc);
undostack_free(&mut stack);
}