Files
oak-editor/crates/oakrender/tests/ffi_contract_test.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

356 lines
12 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! C ABI contract tests (ffi.rs) + manager lifecycle.
mod common;
use std::ffi::c_char;
use std::sync::mpsc;
use std::time::Duration;
use oakrender::error::{
OAKRENDER_E_INVALID, OAKRENDER_E_NOT_FOUND, OAKRENDER_E_STATE, OAKRENDER_OK,
};
use oakrender::ffi;
use oakrender::handle::{alive_count, CHandle};
use std::sync::Mutex;
/// Serializes the alive-count accounting test against handle creation in
/// the other tests of this binary.
static SERIAL: Mutex<()> = Mutex::new(());
/// manager init/shutdown idempotence; available() reflects state.
#[test]
fn manager_lifecycle() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let _g = common::ManagerGuard::init();
unsafe {
// Already initialized → state error.
assert_eq!(ffi::manager::oakrender_manager_init(), OAKRENDER_E_STATE);
assert_eq!(ffi::manager::oakrender_manager_available(), 1);
ffi::manager::oakrender_manager_shutdown();
assert_eq!(ffi::manager::oakrender_manager_available(), 0);
// Re-init works after shutdown (C++ destroy_instance semantics).
assert_eq!(ffi::manager::oakrender_manager_init(), OAKRENDER_OK);
assert_eq!(ffi::manager::oakrender_manager_available(), 1);
}
}
/// Every handle-returning export honors the empty-on-failure and
/// refcount-1-on-success contract (abi_version stamped). Serialized
/// against alive_count_accounting (both create handles).
#[test]
fn handle_contract_all_exports() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
// cache_create: owned, refcount 1, abi stamped.
let c = ffi::cache::oakrender_cache_create();
assert!(!c.is_null());
assert_eq!(c.abi_version, 1);
ffi::cache::oakrender_cache_free(&mut CHandle::null());
let mut cc = c;
ffi::cache::oakrender_cache_free(&mut cc);
assert!(cc.is_null());
// wrap_borrowed(NULL) → empty handle.
assert!(ffi::cache::oakrender_cache_wrap_borrowed(std::ptr::null_mut()).is_null());
// create_for_node: empty parent → empty.
assert!(ffi::cache::oakrender_cache_create_for_node(CHandle::null(), 0).is_null());
// Unknown kind → empty.
assert!(ffi::cache::oakrender_cache_create_for_node(common::fake_handle(1), 99).is_null());
// Non-empty parent + valid kind → owned handle.
let n = ffi::cache::oakrender_cache_create_for_node(common::fake_handle(1), 0);
assert!(!n.is_null());
assert_eq!(n.abi_version, 1);
let mut nn = n;
ffi::cache::oakrender_cache_free(&mut nn);
// codec_frame_create.
let f = ffi::renderer::oakrender_codec_frame_create();
assert!(!f.is_null());
assert_eq!(f.abi_version, 1);
let mut ff = f;
ffi::renderer::oakrender_codec_frame_free(&mut ff);
assert!(ff.is_null());
// cancelatom_init.
let a = ffi::cancelatom::oakrender_cancelatom_init();
assert!(!a.is_null());
assert_eq!(a.abi_version, 1);
let mut aa = a;
ffi::cancelatom::oakrender_cancelatom_free(&mut aa);
assert!(aa.is_null());
// project_copier_create.
let pc = ffi::copier::oakrender_project_copier_create();
assert!(!pc.is_null());
assert_eq!(pc.abi_version, 1);
let mut pp = pc;
ffi::copier::oakrender_project_copier_free(&mut pp);
assert!(pp.is_null());
// renderer create functions.
let r = ffi::renderer::oakrender_display_renderer_create_opengl();
assert!(!r.is_null());
let mut rr = r;
ffi::renderer::oakrender_display_renderer_destroy(&mut rr);
let r2 = ffi::renderer::oakrender_display_renderer_create_dynamic(std::ptr::null());
assert!(r2.is_null(), "NULL backend id → empty");
let r3 = ffi::renderer::oakrender_display_renderer_create_dynamic(c"opengl".as_ptr());
assert!(!r3.is_null());
let mut r3 = r3;
ffi::renderer::oakrender_display_renderer_destroy(&mut r3);
// color processor: NULL strings → empty.
let p = ffi::color::oakrender_color_processor_create(std::ptr::null(), c"x".as_ptr(), 0);
assert!(p.is_null());
// Bad direction → empty.
let p2 = ffi::color::oakrender_color_processor_create(c"a".as_ptr(), c"b".as_ptr(), 7);
assert!(p2.is_null());
}
}
/// free(NULL)/free(empty) no-op across every free export.
#[test]
fn free_null_noop_all_exports() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
ffi::cache::oakrender_cache_free(std::ptr::null_mut());
ffi::renderer::oakrender_codec_frame_free(std::ptr::null_mut());
ffi::renderer::oakrender_display_texture_free(std::ptr::null_mut());
ffi::renderer::oakrender_display_renderer_destroy(std::ptr::null_mut());
ffi::color::oakrender_color_processor_free(std::ptr::null_mut());
ffi::copier::oakrender_project_copier_free(std::ptr::null_mut());
ffi::cancelatom::oakrender_cancelatom_free(std::ptr::null_mut());
ffi::ticket::oakrender_ticket_free(std::ptr::null_mut());
// Empty handles are no-ops too.
let mut empty = CHandle::null();
ffi::cache::oakrender_cache_free(&mut empty);
ffi::renderer::oakrender_codec_frame_free(&mut empty);
ffi::cancelatom::oakrender_cancelatom_free(&mut empty);
}
}
/// Two-stage string getters (disk_cache_path, uuids, filenames):
/// size query, short-buffer rule, exact fit.
#[test]
fn two_stage_string_contract() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
// disk_cache_path: size query then exact copy.
let (size, value) =
common::read_two_stage(|buf, n| ffi::manager::oakrender_disk_cache_path(buf, n));
assert!(size > 0, "required size including NUL");
let path = value.expect("buffer copy");
assert_eq!(path.len() + 1, size as usize);
// Short buffer: size returned, no write.
let mut tiny = [0u8; 4];
let needed = ffi::manager::oakrender_disk_cache_path(tiny.as_mut_ptr() as *mut c_char, 4);
assert_eq!(needed, size);
// cache uuid getter.
let cache = ffi::cache::oakrender_cache_create();
let (usize, uvalue) =
common::read_two_stage(|buf, n| ffi::cache::oakrender_cache_get_uuid(cache, buf, n));
assert!(usize > 0);
let uuid = uvalue.unwrap();
assert_eq!(uuid.len() + 1, usize as usize);
assert!(uuid.starts_with('{') && uuid.ends_with('}'));
// Empty cache → error.
assert_eq!(
ffi::cache::oakrender_cache_get_uuid(CHandle::null(), std::ptr::null_mut(), 0),
OAKRENDER_E_INVALID
);
let mut c = cache;
ffi::cache::oakrender_cache_free(&mut c);
// backend id getters.
let (bsize, bvalue) =
common::read_two_stage(|buf, n| ffi::renderer::oakrender_backend_id_at(0, buf, n));
assert_eq!(bvalue.as_deref(), Some("opengl"));
assert_eq!(bsize, 7); // "opengl" + NUL
assert_eq!(
ffi::renderer::oakrender_backend_id_at(4, std::ptr::null_mut(), 0),
OAKRENDER_E_NOT_FOUND
);
assert_eq!(ffi::renderer::oakrender_backend_count(), 4);
// current_backend is the manager's backend when one is up (parallel
// tests may have initialized it) or the recorded default otherwise;
// either way it is a known id string.
let (_, current) =
common::read_two_stage(|buf, n| ffi::renderer::oakrender_current_backend(buf, n));
let current = current.expect("current backend string");
assert!(
[
"opengl",
"vulkan",
"multiprocess",
"dummy",
"auto",
"metal",
"cpu"
]
.contains(&current.as_str()),
"unexpected current backend {current}"
);
}
}
/// debug_alive_count: cache/texture/processor create+free returns to
/// baseline (leak assertion). Serialized: other tests in this binary
/// create/free handles concurrently.
#[test]
fn alive_count_accounting() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
let baseline = alive_count();
let mut caches = Vec::new();
for _ in 0..4 {
caches.push(ffi::cache::oakrender_cache_create());
}
assert_eq!(alive_count(), baseline + 4);
let mut frames = Vec::new();
for _ in 0..3 {
frames.push(ffi::renderer::oakrender_codec_frame_create());
}
assert_eq!(alive_count(), baseline + 7);
let atom = ffi::cancelatom::oakrender_cancelatom_init();
assert_eq!(alive_count(), baseline + 8);
// Retain doesn't create a new object (just +1 count on the box).
let retained = ffi::renderer::oakrender_codec_frame_retain(frames[0]);
assert_eq!(alive_count(), baseline + 8);
for c in caches.iter_mut() {
ffi::cache::oakrender_cache_free(c);
}
for f in frames.iter_mut() {
ffi::renderer::oakrender_codec_frame_free(f);
}
ffi::renderer::oakrender_codec_frame_free(&mut retained.clone());
let mut atom = atom;
ffi::cancelatom::oakrender_cancelatom_free(&mut atom);
assert_eq!(alive_count(), baseline, "all owned objects released");
}
}
/// Request-frame path: positive id, callback fires with an owned frame,
/// cancel semantics.
#[test]
fn request_frame_and_cancel() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let _g = common::ManagerGuard::init();
unsafe {
let (tx, rx): (mpsc::Sender<u32>, mpsc::Receiver<u32>) = mpsc::channel();
let userdata = Box::into_raw(Box::new(42u32));
extern "C" fn cb(_frame: ffi::OakCodecFrame, ts: i64, userdata: *mut std::ffi::c_void) {
let _ = userdata;
let _ = ts;
}
let id = ffi::manager::oakrender_request_frame(
common::fake_handle(7),
12,
Some(cb),
userdata as *mut std::ffi::c_void,
);
assert!(id > 0, "positive request id, got {id}");
let _ = tx;
drop(rx);
// Wait for the completion: the request's ticket must finish.
std::thread::sleep(Duration::from_millis(200));
// Unknown id → NOT_FOUND.
assert_eq!(
ffi::manager::oakrender_cancel_request(id + 12345),
OAKRENDER_E_NOT_FOUND
);
// Cancelling a finished request is OK (removes from the map).
let rc = ffi::manager::oakrender_cancel_request(id);
assert!(rc == OAKRENDER_OK || rc == OAKRENDER_E_NOT_FOUND);
unsafe { drop(Box::from_raw(userdata)) };
}
// Error paths without a manager (shut down within the guard scope).
unsafe {
ffi::manager::oakrender_manager_shutdown();
assert_eq!(
ffi::manager::oakrender_request_frame(CHandle::null(), 0, None, std::ptr::null_mut()),
OAKRENDER_E_INVALID as i64
);
// Non-null viewer + cb but no manager → STATE.
extern "C" fn cb2(_f: ffi::OakCodecFrame, _t: i64, _u: *mut std::ffi::c_void) {}
assert_eq!(
ffi::manager::oakrender_request_frame(
common::fake_handle(1),
0,
Some(cb2),
std::ptr::null_mut()
),
OAKRENDER_E_STATE as i64
);
}
}
/// set_aggressive_gc + cacher setters behave per contract.
#[test]
fn manager_settings() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let _g = common::ManagerGuard::init();
unsafe {
assert_eq!(
ffi::ticket::oakrender_manager_set_aggressive_gc(1),
OAKRENDER_OK
);
assert_eq!(
ffi::manager::oakrender_set_cacher_multicam(common::fake_handle(3)),
OAKRENDER_OK
);
assert_eq!(
ffi::manager::oakrender_set_cacher_multicam(CHandle::null()),
OAKRENDER_OK
);
assert_eq!(
ffi::manager::oakrender_set_display_color_processor(CHandle::null()),
OAKRENDER_OK
);
ffi::manager::oakrender_cancel_video_tasks(0);
ffi::manager::oakrender_cancel_video_tasks(1);
}
// Without a manager: STATE errors (shut down within the guard scope).
unsafe {
ffi::manager::oakrender_manager_shutdown();
assert_eq!(
ffi::manager::oakrender_set_cacher_multicam(common::fake_handle(1)),
OAKRENDER_E_STATE
);
assert_eq!(
ffi::manager::oakrender_set_display_color_processor(CHandle::null()),
OAKRENDER_E_STATE
);
assert_eq!(
ffi::ticket::oakrender_manager_set_aggressive_gc(0),
OAKRENDER_E_STATE
);
}
}