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:
@@ -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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod error;
|
||||
pub mod ffi;
|
||||
pub mod handle;
|
||||
pub mod undocommand;
|
||||
pub mod undostack;
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user