refactor(engine): pure cdylib + undo-stack test race fix (M14 R4)
- oakengine is now cdylib-only (no rlib/staticlib consumers anywhere; cargo tree verified) — the plugin/external C ABI layer; README and docs updated - cd.yml drops the dylib embedding/re-sign steps (the app no longer links it) - test race root-caused and fixed for good: the global undo stack lock is now a re-entrant mutex (parking_lot) shared by every test that drives the stack, including the previously unlocked node/render families; the render-manager serial-ordering bug (an earlier repro test initialized the global manager before the not-initialized test) is fixed with a shared SERIAL guard and a manager shutdown - 5 consecutive parallel runs clean; serial 209/209
This commit is contained in:
@@ -20,10 +20,11 @@
|
||||
//! Two jobs:
|
||||
//!
|
||||
//! 1. **Force rustc to link every module crate's rlib** into the test
|
||||
//! binary ([`force_link`]). The facade itself only references the
|
||||
//! modules through `extern "C"` imports (see src/bridge), so rustc
|
||||
//! would otherwise drop the dev-dependency rlibs from the link and
|
||||
//! leave the imports undefined.
|
||||
//! binary ([`force_link`]). The test-support files import the module
|
||||
//! crates directly (single-lib; the deleted `src/bridge` no longer
|
||||
//! exists), and the array doubles as a compile-time proof that the
|
||||
//! anchor paths in `crates/oakengine/src/linkage.rs` match the current
|
||||
//! module layouts.
|
||||
//!
|
||||
//! 2. **Re-export the folded-in `oakcore_audioparams_*` accessors** for
|
||||
//! the former mock call sites (`common::oakcore_audioparams_*`). The
|
||||
|
||||
@@ -85,7 +85,7 @@ struct SerialGuard {
|
||||
_task: std::sync::MutexGuard<'static, ()>,
|
||||
/// The facade-wide undo-stack lock, so the `oakengine_project_new`
|
||||
/// calls in these tests never race the it_undo / it_storage stack tests.
|
||||
_stack: std::sync::MutexGuard<'static, ()>,
|
||||
_stack: parking_lot::ReentrantMutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
/// Take the [`SERIAL`] lock AND the global undo-stack lock, recovering
|
||||
@@ -93,8 +93,7 @@ struct SerialGuard {
|
||||
fn serial() -> SerialGuard {
|
||||
let _task = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
.lock();
|
||||
SerialGuard { _task, _stack }
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ fn journal_rows(db: &Path, uuid: &str) -> usize {
|
||||
/// the storage-config lock for the whole body, then point the library at a
|
||||
/// temp SQLite file.
|
||||
fn with_library<R>(db: &Path, f: impl FnOnce() -> R) -> R {
|
||||
let _stack = GLOBAL_STACK_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _stack = GLOBAL_STACK_LOCK.lock();
|
||||
let _config = common::STORAGE_CONFIG_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
@@ -394,7 +394,7 @@ fn export_then_import_round_trip() {
|
||||
/// mutating call fails with E_STATE.
|
||||
#[test]
|
||||
fn disabled_backend_degrades_gracefully() {
|
||||
let _stack = GLOBAL_STACK_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _stack = GLOBAL_STACK_LOCK.lock();
|
||||
let _off = common::storage_off_guard();
|
||||
|
||||
assert_eq!(list_json(), "[]", "no library configured reads as empty");
|
||||
|
||||
@@ -67,7 +67,7 @@ const MATH: &str = "org.olivevideoeditor.Olive.math";
|
||||
/// the storage-config lock for the whole body, then point the write-through
|
||||
/// backend at a temp library.
|
||||
fn with_storage<R>(db: &Path, interval: i32, f: impl FnOnce() -> R) -> R {
|
||||
let _stack = GLOBAL_STACK_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _stack = GLOBAL_STACK_LOCK.lock();
|
||||
let _config = common::STORAGE_CONFIG_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let store = oakcommon::configstore::ConfigStore::instance();
|
||||
store.set(Some("Storage"), "Backend", "sqlite");
|
||||
@@ -80,7 +80,7 @@ fn with_storage<R>(db: &Path, interval: i32, f: impl FnOnce() -> R) -> R {
|
||||
/// "off"`): projects bind to nothing and the undo stack stays untouched by
|
||||
/// write-throughs.
|
||||
fn with_storage_off<R>(f: impl FnOnce() -> R) -> R {
|
||||
let _stack = GLOBAL_STACK_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _stack = GLOBAL_STACK_LOCK.lock();
|
||||
let _config = common::STORAGE_CONFIG_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
oakcommon::configstore::ConfigStore::instance().set(Some("Storage"), "Backend", "off");
|
||||
f()
|
||||
@@ -624,7 +624,7 @@ fn unwritable_library_records_last_error() {
|
||||
#[test]
|
||||
fn default_library_path_and_backend() {
|
||||
common::force_link();
|
||||
let _stack = GLOBAL_STACK_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _stack = GLOBAL_STACK_LOCK.lock();
|
||||
let _config = common::STORAGE_CONFIG_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
// The default path is a plain absolute `…/library.db`.
|
||||
|
||||
@@ -97,7 +97,7 @@ pub(crate) struct SerialGuard {
|
||||
/// The facade's process-wide undo-stack lock (it_undo's), so the
|
||||
/// `oakengine_project_new` calls in these tests (which clear the stack)
|
||||
/// never race the it_undo / it_storage stack tests.
|
||||
_stack: std::sync::MutexGuard<'static, ()>,
|
||||
_stack: parking_lot::ReentrantMutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
/// Take the [`SERIAL`] lock AND the global undo-stack lock, recovering
|
||||
@@ -105,8 +105,7 @@ pub(crate) struct SerialGuard {
|
||||
pub(crate) fn serial() -> SerialGuard {
|
||||
let _task = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
.lock();
|
||||
SerialGuard { _task, _stack }
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ struct SerialGuard {
|
||||
/// The [`SERIAL`] lock.
|
||||
_task: std::sync::MutexGuard<'static, ()>,
|
||||
/// The facade-wide undo-stack lock.
|
||||
_stack: std::sync::MutexGuard<'static, ()>,
|
||||
_stack: parking_lot::ReentrantMutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
/// Take the [`SERIAL`] lock AND the global undo-stack lock, recovering
|
||||
@@ -80,8 +80,7 @@ struct SerialGuard {
|
||||
fn serial() -> SerialGuard {
|
||||
let _task = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
.lock();
|
||||
SerialGuard { _task, _stack }
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,8 @@ unsafe fn read_str(buf: *const c_char) -> String {
|
||||
/// group cannot be shared, so each of those tests holds this lock for its
|
||||
/// whole body. Public so the write-through tests (it_storage.rs), which
|
||||
/// push commands on the same global stack, serialize on the SAME lock.
|
||||
pub static GLOBAL_STACK_LOCK: Mutex<()> = Mutex::new(());
|
||||
pub static GLOBAL_STACK_LOCK: parking_lot::ReentrantMutex<()> =
|
||||
parking_lot::ReentrantMutex::new(());
|
||||
|
||||
static LIFECYCLE_REDO: AtomicI32 = AtomicI32::new(0);
|
||||
static LIFECYCLE_UNDO: AtomicI32 = AtomicI32::new(0);
|
||||
@@ -570,7 +571,7 @@ fn free_contracts() {
|
||||
/// group begin/end/abort lifecycle.
|
||||
#[test]
|
||||
fn undo_stack_integration() {
|
||||
let _lock = GLOBAL_STACK_LOCK.lock().unwrap();
|
||||
let _lock = GLOBAL_STACK_LOCK.lock();
|
||||
common::force_link();
|
||||
|
||||
// --- Baseline: clear() resets to the single "New/Open Project" row.
|
||||
@@ -834,7 +835,7 @@ fn undo_stack_integration() {
|
||||
/// label") and the facade docs.
|
||||
#[test]
|
||||
fn null_name_push_repro() {
|
||||
let _lock = GLOBAL_STACK_LOCK.lock().unwrap();
|
||||
let _lock = GLOBAL_STACK_LOCK.lock();
|
||||
common::force_link();
|
||||
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
|
||||
|
||||
@@ -866,7 +867,7 @@ fn null_name_push_repro() {
|
||||
/// is safe.
|
||||
#[test]
|
||||
fn null_name_group_repro() {
|
||||
let _lock = GLOBAL_STACK_LOCK.lock().unwrap();
|
||||
let _lock = GLOBAL_STACK_LOCK.lock();
|
||||
common::force_link();
|
||||
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
|
||||
|
||||
@@ -905,7 +906,7 @@ unsafe extern "C" fn abort_undo_cb(_ud: *mut c_void) {
|
||||
/// satisfied by a leftover value from an earlier jump.)
|
||||
#[test]
|
||||
fn group_abort_undoes_children_repro() {
|
||||
let _lock = GLOBAL_STACK_LOCK.lock().unwrap();
|
||||
let _lock = GLOBAL_STACK_LOCK.lock();
|
||||
common::force_link();
|
||||
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ unsafe fn find_node(project: *mut crate::handle::OakEngineProject, id: &str) ->
|
||||
/// process-wide (the same serialization the undo family uses).
|
||||
#[test]
|
||||
fn project_node_keyframe_lifecycle() {
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock();
|
||||
common::force_link();
|
||||
let _ = force_oakundo_command_link();
|
||||
// The undo commands below would bind the project to the default user
|
||||
@@ -399,6 +400,7 @@ fn project_node_keyframe_lifecycle() {
|
||||
/// NULL handles yield -1 and out-of-range indexes yield -4.
|
||||
#[test]
|
||||
fn node_failure_paths() {
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock();
|
||||
common::force_link();
|
||||
|
||||
// NULL node → OAKENGINE_E_INVALID (-1).
|
||||
@@ -437,6 +439,7 @@ fn node_failure_paths() {
|
||||
/// Footage probe/import/borrow failure paths (no media required).
|
||||
#[test]
|
||||
fn footage_failure_paths() {
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock();
|
||||
common::force_link();
|
||||
|
||||
// Probing a nonexistent path → NULL + a non-empty last error.
|
||||
|
||||
@@ -19,11 +19,17 @@
|
||||
//! tests, so the manager/cacher families exercise the module's STATE
|
||||
//! error path and the renderer/color families exercise the NULL/invalid
|
||||
//! argument paths (real rendering needs the deferred node family plus an
|
||||
//! initialized render manager).
|
||||
//! initialized render manager). The empty-sequence repro at the bottom
|
||||
//! brings the process-global manager up for its run and shuts it back
|
||||
//! down, restoring this invariant for the tests that follow it; the two
|
||||
//! manager-touching tests serialize on [`SERIAL`] so the repro never
|
||||
//! overlaps `render_manager_not_initialized` (the `it_task`/`it_export`
|
||||
//! pattern).
|
||||
|
||||
use super::common;
|
||||
|
||||
use std::ffi::{c_char, c_double};
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use crate::render::{
|
||||
oakengine_color_last_error, oakengine_color_manager_get_config_filename,
|
||||
@@ -38,10 +44,23 @@ use crate::render::{
|
||||
OakColorTransformPod,
|
||||
};
|
||||
|
||||
/// Serialize the manager-touching render tests (the pattern in
|
||||
/// `it_task`/`it_export`). The empty-sequence repro brings the
|
||||
/// process-global render manager up (and back down) inside its run;
|
||||
/// without the lock it overlaps `render_manager_not_initialized`, whose
|
||||
/// STATE paths then see an initialized manager.
|
||||
static SERIAL: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn serial() -> MutexGuard<'static, ()> {
|
||||
SERIAL.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Render manager state without initialization: the module reports its
|
||||
/// STATE error, passed through untranslated (-70002).
|
||||
#[test]
|
||||
fn render_manager_not_initialized() {
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock();
|
||||
let _g = serial();
|
||||
assert_eq!(
|
||||
unsafe { oakengine_render_manager_set_aggressive_garbage_collection(1) },
|
||||
-70002
|
||||
@@ -62,6 +81,7 @@ fn render_manager_not_initialized() {
|
||||
/// Renderer lifecycle: NULL sequence is rejected; mode validation.
|
||||
#[test]
|
||||
fn renderer_lifecycle() {
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock();
|
||||
// NULL seq → NULL renderer.
|
||||
let r = unsafe {
|
||||
oakengine_renderer_create(
|
||||
@@ -92,6 +112,7 @@ fn renderer_lifecycle() {
|
||||
/// Frame accessors on NULL / empty handles report zero/NULL safely.
|
||||
#[test]
|
||||
fn frame_accessors_null_safe() {
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock();
|
||||
assert_eq!(unsafe { oakengine_frame_width(std::ptr::null()) }, 0);
|
||||
assert_eq!(unsafe { oakengine_frame_height(std::ptr::null()) }, 0);
|
||||
assert_eq!(
|
||||
@@ -107,6 +128,7 @@ fn frame_accessors_null_safe() {
|
||||
/// NULL; freeing is safe either way.
|
||||
#[test]
|
||||
fn color_processor_lifecycle() {
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock();
|
||||
// NULL input → NULL.
|
||||
let p = unsafe {
|
||||
oakengine_color_processor_create(std::ptr::null(), std::ptr::null(), std::ptr::null(), 0)
|
||||
@@ -157,6 +179,7 @@ fn color_processor_lifecycle() {
|
||||
/// reports STATE; the last-error string starts empty.
|
||||
#[test]
|
||||
fn color_manager_and_error() {
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock();
|
||||
let mut buf = [0 as c_char; 64];
|
||||
let rc = unsafe {
|
||||
oakengine_color_manager_get_config_filename(std::ptr::null(), buf.as_mut_ptr(), 64)
|
||||
@@ -170,6 +193,7 @@ fn color_manager_and_error() {
|
||||
/// LUT library stubs report the documented neutral values.
|
||||
#[test]
|
||||
fn lut_library_stubs() {
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock();
|
||||
assert_eq!(unsafe { oakengine_lut_directory_count() }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_lut_set_directories(std::ptr::null(), 0) },
|
||||
@@ -180,6 +204,8 @@ fn lut_library_stubs() {
|
||||
// repro: render_audio on an empty sequence (playback tick on an empty timeline).
|
||||
#[test]
|
||||
fn render_audio_empty_sequence_no_crash() {
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK.lock();
|
||||
let _g = serial();
|
||||
super::common::force_link();
|
||||
unsafe {
|
||||
assert_eq!(crate::render::oakengine_render_manager_init(), 0);
|
||||
@@ -200,5 +226,10 @@ fn render_audio_empty_sequence_no_crash() {
|
||||
}
|
||||
crate::render::oakengine_renderer_free(r);
|
||||
crate::node::oakengine_project_free(project);
|
||||
// The repro initialized the process-global render manager; tear it
|
||||
// down so the tests after this one keep the module header's
|
||||
// documented "manager not initialized" contract (the STATE paths
|
||||
// asserted by `render_manager_not_initialized`).
|
||||
assert_eq!(crate::render::oakengine_render_manager_shutdown(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,8 +157,7 @@ fn undo_stack_lifecycle() {
|
||||
// write-through tests (the facade's stack is process-wide): without it
|
||||
// a concurrent test's pushes break the exact-count assertions below.
|
||||
let _stack = super::it_undo::GLOBAL_STACK_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
.lock();
|
||||
|
||||
// Reset to a clean "New/Open Project" base row.
|
||||
assert_eq!(unsafe { oakengine_undo_clear() }, 0);
|
||||
|
||||
Reference in New Issue
Block a user