// 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 .
//! FFI 出口层契约测试:instance.h 面(含 M11 §2.1 内省族)。
//!
//! 宿主单例无锁:全部用例经 [`common::with_host`] 串行化。
//! render 路径经 oakrender 测试桩(`--features test-stubs`);
//! 未启用时相关用例 skip。
mod common;
use std::ffi::{c_char, c_int, c_void, CStr, CString};
use oakplugin::ffi::{
node_value_type, oakplugin_debug_alive_count, oakplugin_instance_cancel,
oakplugin_instance_clip_count, oakplugin_instance_clip_info, oakplugin_instance_clip_name,
oakplugin_instance_create, oakplugin_instance_free, oakplugin_instance_get_param,
oakplugin_instance_get_param_string, oakplugin_instance_param_choice_count,
oakplugin_instance_param_choice_label, oakplugin_instance_param_count,
oakplugin_instance_param_default_double, oakplugin_instance_param_default_string,
oakplugin_instance_param_display_range, oakplugin_instance_param_hint,
oakplugin_instance_param_label, oakplugin_instance_param_name, oakplugin_instance_param_parent,
oakplugin_instance_param_secret, oakplugin_instance_param_type, oakplugin_instance_render,
oakplugin_instance_set_param, oakplugin_instance_set_param_string,
oakplugin_instance_set_progress_cb, OakNodeValue,
};
use oakplugin::handle::CHandle;
const E_INVALID: i32 = -90001;
const E_NOT_FOUND: i32 = -90004;
const OK: i32 = 0;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
/// 扫描测试插件并创建实例;不可用返回空句柄。
fn create_instance() -> CHandle {
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
return CHandle::null();
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
unsafe { oakplugin_host_scan_import(dirs.as_ptr(), 1) };
let id = cs(TEST_PLUGIN_ID);
unsafe { oakplugin_instance_create(id.as_ptr()) }
}
// 避免重复 import host_scan(ffi 模块内路径统一)。
use oakplugin::ffi::oakplugin_host_scan as oakplugin_host_scan_import;
/// 两段式读字符串。
fn two_stage(
get: unsafe extern "C" fn(CHandle, c_int, *mut c_char, c_int) -> c_int,
h: CHandle,
index: c_int,
) -> Option {
let len = unsafe { get(h, index, std::ptr::null_mut(), 0) };
if len <= 0 {
return None;
}
let mut buf = vec![0u8; len as usize];
let r = unsafe { get(h, index, buf.as_mut_ptr() as *mut c_char, len) };
if r != OK {
return None;
}
Some(
unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }
.to_string_lossy()
.into_owned(),
)
}
/// 简易实例句柄(测试桩纹理用)。
fn fake_texture(ctx: usize) -> CHandle {
CHandle {
ctx: ctx as *mut c_void,
addref: None,
release: None,
abi_version: 1,
}
}
/// create/free:已知插件返回非空句柄;free 后 alive 回基线;
/// free(NULL)/free(空) no-op。
#[test]
fn instance_create_free() {
common::with_host(|| {
unsafe { oakplugin_host_scan_import(std::ptr::null(), 0) };
let base = unsafe { oakplugin_debug_alive_count() };
let mut h = create_instance();
assert!(!h.is_null());
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base + 1);
unsafe { oakplugin_instance_free(&mut h) };
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base);
// free(NULL)/free(空) no-op。
unsafe { oakplugin_instance_free(std::ptr::null_mut()) };
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// create 未知标识:返回空句柄(init 失败返回空,契约 §a)。
#[test]
fn instance_create_unknown_id() {
common::with_host(|| {
let id = cs("org.oak.does-not-exist");
let h = unsafe { oakplugin_instance_create(id.as_ptr()) };
assert!(h.is_null());
});
}
/// set/get_param(double):round-trip 一致;未知参数名
/// E_NOT_FOUND;空句柄 E_INVALID;out 为 NULL 时 E_INVALID。
#[test]
fn instance_param_double_roundtrip() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let name = cs("gain");
let mut v = OakNodeValue::default();
v.r#type = node_value_type::FLOAT;
v.f = [1.25, 0.0, 0.0, 0.0];
assert_eq!(
unsafe { oakplugin_instance_set_param(h, name.as_ptr(), &v) },
OK
);
let mut out = OakNodeValue::default();
assert_eq!(
unsafe { oakplugin_instance_get_param(h, name.as_ptr(), &mut out) },
OK
);
assert_eq!(out.r#type, node_value_type::FLOAT);
assert_eq!(out.f[0], 1.25);
// 未知参数名 → E_NOT_FOUND。
let nope = cs("nope");
assert_eq!(
unsafe { oakplugin_instance_set_param(h, nope.as_ptr(), &v) },
E_NOT_FOUND
);
// 空句柄 → E_INVALID。
assert_eq!(
unsafe { oakplugin_instance_set_param(CHandle::null(), name.as_ptr(), &v) },
E_INVALID
);
// out NULL → E_INVALID。
assert_eq!(
unsafe { oakplugin_instance_get_param(h, name.as_ptr(), std::ptr::null_mut()) },
E_INVALID
);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// set/get_param_string:round-trip 一致(含空串与长串两段式)。
#[test]
fn instance_param_string_roundtrip() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let name = cs("label");
let value = cs("hello 你好");
assert_eq!(
unsafe { oakplugin_instance_set_param_string(h, name.as_ptr(), value.as_ptr()) },
OK
);
// 两段式。
let len = unsafe {
oakplugin_instance_get_param_string(h, name.as_ptr(), std::ptr::null_mut(), 0)
};
assert!(len > 0);
let mut buf = vec![0u8; len as usize];
assert_eq!(
unsafe {
oakplugin_instance_get_param_string(
h,
name.as_ptr(),
buf.as_mut_ptr() as *mut c_char,
len,
)
},
OK
);
assert_eq!(
unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_string_lossy(),
"hello 你好"
);
// 空串。
let empty = cs("");
assert_eq!(
unsafe { oakplugin_instance_set_param_string(h, name.as_ptr(), empty.as_ptr()) },
OK
);
assert_eq!(
unsafe {
oakplugin_instance_get_param_string(h, name.as_ptr(), std::ptr::null_mut(), 0)
},
1
);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// render:对测试插件渲一帧,输出纹理像素符合插件的常量填充
/// (0.5 RGBA F32,alpha=1)。time 为 NaN 时 E_INVALID。
#[cfg(feature = "test-stubs")]
#[test]
fn instance_render_one_frame() {
common::with_host(|| {
use oakplugin::bridge::render::stub;
let mut h = create_instance();
if h.is_null() {
return;
}
// 输出帧 320×240 F32。
stub::setup_dst(320, 240, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
let src = CHandle::null(); // 无输入(插件只写常量)
assert_eq!(unsafe { oakplugin_instance_render(h, dst, src, 0.0) }, OK);
// 断言:常量 0.5,alpha 1.0(每像素 4 通道 × 4 字节 F32)。
let pixels = stub::dst_pixels();
let n = pixels.len() / 16;
assert_eq!(n, 320 * 240, "像素数 = len/16 = {n}");
for i in 0..n {
// 像素基址 = i * 16(4 通道 × 4 字节)。
let f = |o: usize| {
f32::from_le_bytes(
pixels[i * 16 + o * 4..i * 16 + o * 4 + 4]
.try_into()
.unwrap(),
)
};
assert_eq!(f(0), 0.5, "pixel {i} r");
assert_eq!(f(1), 0.5, "pixel {i} g");
assert_eq!(f(2), 0.5, "pixel {i} b");
assert_eq!(f(3), 1.0, "pixel {i} a");
}
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// render 默认构建(真实 oakrender 链接):NULL 输出纹理在触达
/// oakrender 之前被插件层校验拒绝(E_INVALID),不崩溃。真实纹理
/// 路径需要 GL/GPU,属集成测试范畴。
#[cfg(not(feature = "test-stubs"))]
#[test]
fn instance_render_one_frame() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let dst = CHandle::null();
assert_eq!(
unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) },
-90001
);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// progress 回调:render 期间被调用且进度单调不减;回调返回非 0
/// 时 render 以取消码结束(插件检查 progressUpdate 结果并中止)。
#[cfg(feature = "test-stubs")]
#[test]
fn instance_progress_callback() {
common::with_host(|| {
use oakplugin::bridge::render::stub;
let mut h = create_instance();
if h.is_null() {
return;
}
let mut seen: Vec = Vec::new();
unsafe extern "C" fn capture(p: f64, userdata: *mut c_void) -> c_int {
let v = unsafe { &mut *(userdata as *mut Vec) };
v.push(p);
0
}
unsafe {
oakplugin_instance_set_progress_cb(h, Some(capture), &mut seen as *mut _ as *mut c_void)
};
stub::setup_dst(64, 64, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
assert_eq!(
unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) },
OK
);
assert_eq!(seen, vec![0.5], "插件在 render 内报一次 0.5");
// 取消回调(非 0 = 中止)。
unsafe extern "C" fn abort_cb(_p: f64, _u: *mut c_void) -> c_int {
1
}
unsafe { oakplugin_instance_set_progress_cb(h, Some(abort_cb), std::ptr::null_mut()) };
stub::setup_dst(64, 64, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let r = unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) };
assert_eq!(r, -90003, "取消应使 render 失败");
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// cancel:无活动渲染时 no-op;置位后 render 立即以取消码返回。
#[test]
fn instance_cancel_semantics() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
// 无活动渲染:OK no-op。
assert_eq!(unsafe { oakplugin_instance_cancel(h) }, OK);
// 置位后 render 入口即取消(先备好合法输出帧,确保走到
// render 的取消检查而不是帧校验)。
assert_eq!(unsafe { oakplugin_instance_cancel(h) }, OK);
#[cfg(feature = "test-stubs")]
oakplugin::bridge::render::stub::setup_dst(
64,
64,
oakplugin::bridge::render::PIXEL_FORMAT_F32,
);
let dst = fake_texture(0xA1);
assert_eq!(
unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) },
-90003
);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 内省:param_count > 0;name/type/label/hint/parent 与插件定义
/// 一致;index 越界 E_NOT_FOUND。
#[test]
fn introspection_param_fields() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let count = unsafe { oakplugin_instance_param_count(h) };
assert!(count >= 4, "gain/mode/debug/label,count={count}");
// gain 的字段。
assert_eq!(
two_stage(oakplugin_instance_param_name, h, 0),
Some("gain".into())
);
assert_eq!(
two_stage(oakplugin_instance_param_type, h, 0),
Some("OfxParamTypeDouble".into())
);
assert_eq!(
two_stage(oakplugin_instance_param_label, h, 0),
Some("Gain".into())
);
assert_eq!(
two_stage(oakplugin_instance_param_hint, h, 0),
Some(String::new())
);
assert_eq!(
two_stage(oakplugin_instance_param_parent, h, 0),
Some(String::new())
);
// 越界 → E_NOT_FOUND。
assert_eq!(
unsafe { oakplugin_instance_param_name(h, count, std::ptr::null_mut(), 0) },
E_NOT_FOUND
);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 内省:secret/display_range/choice 三族在测试插件的对应参数上
/// 逐字段断言。
#[test]
fn introspection_secret_range_choice() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let count = unsafe { oakplugin_instance_param_count(h) };
// 按名定位索引。
let mut gain = -1;
let mut mode = -1;
let mut debug = -1;
for i in 0..count {
match two_stage(oakplugin_instance_param_name, h, i).as_deref() {
Some("gain") => gain = i,
Some("mode") => mode = i,
Some("debug") => debug = i,
_ => {}
}
}
assert!(gain >= 0 && mode >= 0 && debug >= 0);
// display range(gain:-2..2)。
let (mut min, mut max) = (0.0, 0.0);
assert_eq!(
unsafe { oakplugin_instance_param_display_range(h, gain, &mut min, &mut max) },
OK
);
assert_eq!((min, max), (-2.0, 2.0));
// secret(debug = 1;gain = 0)。
let mut s = 0;
assert_eq!(
unsafe { oakplugin_instance_param_secret(h, debug, &mut s) },
OK
);
assert_eq!(s, 1);
assert_eq!(
unsafe { oakplugin_instance_param_secret(h, gain, &mut s) },
OK
);
assert_eq!(s, 0);
// choice(mode:2 个选项;选项内容见 introspection_choice_labels)。
assert_eq!(unsafe { oakplugin_instance_param_choice_count(h, mode) }, 2);
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// choice label/value 的带选项两段式。
#[test]
fn introspection_choice_labels() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let count = unsafe { oakplugin_instance_param_count(h) };
let mut mode = -1;
for i in 0..count {
if two_stage(oakplugin_instance_param_name, h, i).as_deref() == Some("mode") {
mode = i;
}
}
assert!(mode >= 0);
assert_eq!(unsafe { oakplugin_instance_param_choice_count(h, mode) }, 2);
for c in 0..2 {
let len = unsafe {
oakplugin_instance_param_choice_label(h, mode, c, std::ptr::null_mut(), 0)
};
assert!(len > 0);
let mut buf = vec![0u8; len as usize];
assert_eq!(
unsafe {
oakplugin_instance_param_choice_label(
h,
mode,
c,
buf.as_mut_ptr() as *mut c_char,
len,
)
},
OK
);
let s = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }
.to_string_lossy()
.into_owned();
assert!(s == "Fast" || s == "High", "选项 {c} = {s}");
}
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 内省:默认值(double 族与字符串)。
#[test]
fn introspection_defaults() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
let count = unsafe { oakplugin_instance_param_count(h) };
let mut gain = -1;
let mut label = -1;
for i in 0..count {
match two_stage(oakplugin_instance_param_name, h, i).as_deref() {
Some("gain") => gain = i,
Some("label") => label = i,
_ => {}
}
}
let mut d = 99.0;
assert_eq!(
unsafe { oakplugin_instance_param_default_double(h, gain, 0, &mut d) },
OK
);
assert_eq!(d, 0.0);
let s = two_stage(oakplugin_instance_param_default_string, h, label);
assert_eq!(s, Some(String::new()));
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// 内省:clip_count/name/info(含 Output clip;optional 标记)。
#[test]
fn introspection_clips() {
common::with_host(|| {
let mut h = create_instance();
if h.is_null() {
return;
}
assert_eq!(unsafe { oakplugin_instance_clip_count(h) }, 2);
let names: Vec = (0..2)
.map(|i| two_stage(oakplugin_instance_clip_name, h, i).unwrap_or_default())
.collect();
assert!(names.contains(&"Source".into()), "{names:?}");
assert!(names.contains(&"Output".into()), "{names:?}");
for i in 0..2 {
let mut optional = -1;
let len = unsafe {
oakplugin_instance_clip_info(h, i, &mut optional, std::ptr::null_mut(), 0)
};
assert!(len > 0);
let mut buf = vec![0u8; len as usize];
assert_eq!(
unsafe {
oakplugin_instance_clip_info(
h,
i,
&mut optional,
buf.as_mut_ptr() as *mut c_char,
len,
)
},
OK
);
assert!(optional == 0, "测试插件 clip 非 optional");
}
unsafe { oakplugin_instance_free(&mut h) };
});
}
/// debug_alive_count:create 增、free 减;全部释放后回零基线。
#[test]
fn alive_count_accounting() {
common::with_host(|| {
unsafe { oakplugin_host_scan_import(std::ptr::null(), 0) };
let base = unsafe { oakplugin_debug_alive_count() };
let mut h = create_instance();
if h.is_null() {
return;
}
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base + 1);
unsafe { oakplugin_instance_free(&mut h) };
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base);
});
}