Files
oak-editor/crates/oakplugin/tests/ffi_host_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

232 lines
7.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/>.
//! FFI 出口层(ffi.rs)契约测试:host.h 面。
//!
//! 每个导出函数至少一正常一错误路径(项目铁律)。
//! 宿主单例无锁:全部用例经 [`common::with_host`] 串行化。
//! 需要最小测试插件(build.rs 编,common::test_plugin_dir 装配);
//! 不可用时 skip。
mod common;
use std::ffi::{c_char, c_int, CStr, CString};
use oakplugin::ffi::{
oakplugin_host_init, oakplugin_host_plugin_count, oakplugin_host_plugin_id_at,
oakplugin_host_plugin_label, oakplugin_host_scan, oakplugin_host_set_message_handler,
oakplugin_host_shutdown,
};
const E_INVALID: i32 = -90001;
const E_NOT_FOUND: i32 = -90004;
const OK: i32 = 0;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// 扫描测试插件;失败/缺失返回 false(调用方 skip)。
fn scan_test_plugin() -> bool {
let Some(dir) = common::test_plugin_scan_dir() else {
common::skip("最小测试插件未构建");
return false;
};
let dir = cs(dir.to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) == OK }
}
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
/// init/shutdown 幂等:重复 init 无副作用;shutdown 后再 init 可用。
#[test]
fn host_init_shutdown_idempotent() {
common::with_host(|| {
assert_eq!(unsafe { oakplugin_host_init() }, OK);
assert_eq!(unsafe { oakplugin_host_init() }, OK);
unsafe { oakplugin_host_shutdown() };
assert_eq!(unsafe { oakplugin_host_init() }, OK);
unsafe { oakplugin_host_shutdown() };
});
}
/// scan 默认路径(NULL/0):返回 OAKPLUGIN_OK 或 E_FAILED(无插件
/// 目录的机器),二者之外不允许别的码。
#[test]
fn host_scan_default_paths() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let r = unsafe { oakplugin_host_scan(std::ptr::null(), 0) };
assert!(r == OK || r == -90003, "scan(NULL,0) = {r}");
unsafe { oakplugin_host_shutdown() };
});
}
/// scan 指定目录(测试插件所在):插件计数 ≥1 且能找到测试插件
/// 标识;目录不存在返回 E_FAILED 而不崩。
#[test]
fn host_scan_explicit_dir() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
assert!(scan_test_plugin(), "测试插件扫描失败");
assert!(unsafe { oakplugin_host_plugin_count() } >= 1);
// 标识可见(两段式第一段)。
let len = unsafe { oakplugin_host_plugin_id_at(0, std::ptr::null_mut(), 0) };
assert!(len > 0);
let mut buf = vec![0u8; len as usize];
let r = unsafe { oakplugin_host_plugin_id_at(0, buf.as_mut_ptr() as *mut c_char, len) };
assert_eq!(r, OK);
assert_eq!(
unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }
.to_str()
.unwrap(),
TEST_PLUGIN_ID
);
// 目录不存在:静默跳过返回 OK 而不崩(C++ olivehost.cpp:59-62
// 的 add_plugin_path 语义——声明原期望 E_FAILED,与参照系
// 不符,以 C++ 行为为准)。
let nope = cs("/nonexistent/ofx/plugins");
let dirs = [nope.as_ptr()];
assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK);
unsafe { oakplugin_host_shutdown() };
});
}
/// plugin_id_at:两段式(先问长度再取内容);越界索引返回
/// E_NOT_FOUND;缓冲区不足时返回所需长度且不越界写。
#[test]
fn host_plugin_id_at_two_stage() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
if !scan_test_plugin() {
unsafe { oakplugin_host_shutdown() };
return;
}
// 越界 → E_NOT_FOUND。
assert_eq!(
unsafe { oakplugin_host_plugin_id_at(99, std::ptr::null_mut(), 0) },
E_NOT_FOUND
);
// 缓冲区不足:返回所需长度(> 0),不越界写(哨兵在 [4]——
// [3] 是 4 字节缓冲内的 NUL 位)。
let mut small = [0x7fu8; 5];
let r = unsafe { oakplugin_host_plugin_id_at(0, small.as_mut_ptr() as *mut c_char, 4) };
assert!(r > 4);
assert_eq!(small[4], 0x7f, "缓冲区外不应被写");
unsafe { oakplugin_host_shutdown() };
});
}
/// plugin_label:已知插件返回非空 label;未知标识 E_NOT_FOUND
/// NULL 参数 E_INVALID。
#[test]
fn host_plugin_label_paths() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
if !scan_test_plugin() {
unsafe { oakplugin_host_shutdown() };
return;
}
let id = cs(TEST_PLUGIN_ID);
let len = unsafe { oakplugin_host_plugin_label(id.as_ptr(), std::ptr::null_mut(), 0) };
assert!(len > 0);
let mut buf = vec![0u8; len as usize];
assert_eq!(
unsafe {
oakplugin_host_plugin_label(id.as_ptr(), buf.as_mut_ptr() as *mut c_char, len)
},
OK
);
assert!(!unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }
.to_bytes()
.is_empty());
// 未知标识。
let nope = cs("org.oak.nope");
assert_eq!(
unsafe { oakplugin_host_plugin_label(nope.as_ptr(), std::ptr::null_mut(), 0) },
E_NOT_FOUND
);
// NULL 参数。
assert_eq!(
unsafe { oakplugin_host_plugin_label(std::ptr::null(), std::ptr::null_mut(), 0) },
E_INVALID
);
unsafe { oakplugin_host_shutdown() };
});
}
/// message handler:注册后测试插件的 message 回调到达(级别与
/// 文本透传,含 %d 格式化);注册 NULL 后消息被丢弃而不崩。
#[test]
fn host_message_handler_dispatch() {
common::with_host(|| {
use oakplugin::ffi::{oakplugin_instance_create, oakplugin_instance_free};
unsafe extern "C" fn capture(
type_: *const c_char,
message: *const c_char,
userdata: *mut std::ffi::c_void,
) -> c_int {
let v = unsafe { &mut *(userdata as *mut Vec<(String, String)>) };
v.push((
unsafe { CStr::from_ptr(type_) }
.to_string_lossy()
.into_owned(),
unsafe { CStr::from_ptr(message) }
.to_string_lossy()
.into_owned(),
));
0
}
unsafe { oakplugin_host_init() };
if !scan_test_plugin() {
unsafe { oakplugin_host_shutdown() };
return;
}
// 注册捕获器 → 创建实例(插件在 createInstance 发 message)。
let mut captured: Vec<(String, String)> = Vec::new();
unsafe {
oakplugin_host_set_message_handler(
Some(capture),
&mut captured as *mut _ as *mut std::ffi::c_void,
)
};
let id = cs(TEST_PLUGIN_ID);
let mut h = unsafe { oakplugin_instance_create(id.as_ptr()) };
assert!(!h.is_null());
assert_eq!(captured.len(), 1);
assert_eq!(captured[0].0, "OfxMessageMessage");
assert_eq!(captured[0].1, "created id=7", "v1 变长格式化应生效");
// 注销 → 再创建不崩、无捕获。
unsafe { oakplugin_host_set_message_handler(None, std::ptr::null_mut()) };
captured.clear();
let mut h2 = unsafe { oakplugin_instance_create(id.as_ptr()) };
assert!(!h2.is_null());
assert!(captured.is_empty());
unsafe { oakplugin_instance_free(&mut h2) };
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}