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

765 lines
22 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/>.
//! suite 层 round-trip 测试:宿主 suite 表 ⇄ 最小测试插件。
//!
//! 声明文档原计划"每个用例让测试插件在 describe/render 期间真实
//! 回调对应 suite""插件视角"的 HostSupport 兼容性背书)。最小
//! 测试插件(tests/fixtures/oak-test-plugin.ofx.bundle)是第 0 期
//! 交付物,尚未构建——插件无关的断言直接执行;依赖插件的用例经
//! [`common::skip`] 门,插件落地后补全(用例内 `// TODO(plugin)`)。
mod common;
use std::ffi::{c_char, c_double, c_int, c_uint, c_void, CStr, CString};
use std::sync::atomic::{AtomicUsize, Ordering};
use oakplugin::descriptor::EffectDescriptor;
use oakplugin::instance::Instance;
use oakplugin::param::{ParamInstance, ParamSetInstance};
use oakplugin::property::{PropertySet, Value};
use oakplugin::suites::fetch_suite;
use oakplugin::suites::image_effect::suite_v1 as image_effect_suite;
use oakplugin::suites::memory::suite_v1 as memory_suite;
use oakplugin::suites::message::suite_v1 as message_suite_v1;
use oakplugin::suites::multithread::suite_v1 as multithread_suite;
use oakplugin::suites::param::suite_v1 as param_suite;
use oakplugin::suites::progress::suite_v1 as progress_suite;
use oakplugin::suites::property::suite_v1 as property_suite;
use oakplugin::suites::tag;
use oakplugin::suites::timeline::suite_v1 as timeline_suite;
/// OFX 状态码的本地别名(SDK ofxCore.h)。
const OK: c_int = 0;
const UNKNOWN: c_int = 3;
const UNSUPPORTED: c_int = 5;
const BAD_HANDLE: c_int = 9;
const BAD_INDEX: c_int = 10;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
/// 未打标属性集句柄(宿主内部直用路径)。
fn props_handle(s: &PropertySet) -> *mut c_void {
s as *const PropertySet as *mut c_void
}
/// 假插件(仅喂给 Instancedescribe 之外的字段不被触碰)。
fn dummy_plugin(descriptor: EffectDescriptor) -> std::sync::Arc<oakplugin::host::Plugin> {
unsafe extern "C" fn dummy_entry(
_: *const c_char,
_: *const c_void,
_: *mut c_void,
_: *mut c_void,
) -> c_int {
OK
}
std::sync::Arc::new(oakplugin::host::Plugin {
identifier: "test.plugin".into(),
version: (1, 0),
bundle_path: std::path::PathBuf::new(),
contexts: vec![],
descriptor,
lib: std::ptr::null_mut(),
entry: dummy_entry,
ofx_plugin: std::ptr::null_mut(),
})
}
/// 用 param suite 的 paramDefine 造一个含 6 参数的实例。
fn make_instance() -> (std::sync::Arc<Instance>, *mut c_void) {
let mut desc = EffectDescriptor::new();
let s = param_suite();
let dhandle = tag::make(&desc.props as *const PropertySet, tag::DESCRIPTOR);
unsafe {
for (t, n) in [
("OfxParamTypeInteger", "gain"),
("OfxParamTypeDouble", "opacity"),
("OfxParamTypeDouble2D", "pos"),
("OfxParamTypeRGB", "color"),
("OfxParamTypeBoolean", "enabled"),
("OfxParamTypeString", "label"),
] {
let t = cs(t);
let n = cs(n);
let mut ph: *mut c_void = std::ptr::null_mut();
assert_eq!(
(s.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut ph),
OK
);
}
}
let params = ParamSetInstance {
params: desc
.params
.iter()
.map(|d| Box::new(ParamInstance::from_def((**d).clone())))
.collect(),
};
let plugin = dummy_plugin(desc);
let inst = std::sync::Arc::new(Instance {
props: PropertySet::new(),
plugin,
context: "OfxImageEffectContextFilter".into(),
params,
clips: vec![],
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(oakplugin::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
});
let h = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
(inst, h)
}
/// fetch_suite:第 1 期承诺的八张表按名按版本可取(v 不符返回
/// 空);不认识的 suite 返回空。覆盖 M11 §3.1 清单逐个断言。
#[test]
fn fetch_suite_registry() {
for (name, version) in [
("OfxPropertySuite", 1),
("OfxMemorySuite", 1),
("OfxImageEffectSuite", 1),
("OfxParameterSuite", 1),
("OfxMessageSuite", 1),
("OfxMessageSuite", 2),
("OfxProgressSuite", 1),
("OfxProgressSuite", 2),
("OfxTimeLineSuite", 1),
("OfxMultiThreadSuite", 1),
] {
assert!(
fetch_suite(name, version).is_some(),
"{name} v{version} 应可取"
);
}
assert!(fetch_suite("OfxPropertySuite", 2).is_none());
assert!(fetch_suite("OfxMessageSuite", 3).is_none());
assert!(fetch_suite("OfxBogusSuite", 1).is_none());
}
/// property suitedefine→set→get→getDimension→reset 全链路;
/// 越界读返回 kOfxStatErrBadIndex;未定义属性 → ErrUnknown。
///
/// 声明原含"写只读宿主能力属性 → kOfxStatErrReadOnly"SDK 无此
/// 状态码、HostSupport 也不在 suite 层拦截写(见 suites/property.rs
/// 模块文档的偏差说明)——本测试随之改为断言 reset 的明确行为。
#[test]
fn property_suite_roundtrip() {
let set = PropertySet::new();
set.define("width", vec![Value::Int(0)]);
let h = props_handle(&set);
let s = property_suite();
let name = cs("width");
// set → get round-trip。
unsafe {
assert_eq!((s.set_int)(h, name.as_ptr(), 0, 1920), OK);
}
let mut v = 0;
unsafe {
assert_eq!((s.get_int)(h, name.as_ptr(), 0, &mut v), OK);
}
assert_eq!(v, 1920);
// getDimension。
let mut dim = 0;
unsafe {
assert_eq!((s.get_dimension)(h, name.as_ptr(), &mut dim), OK);
}
assert_eq!(dim, 1);
// 越界读 → BadIndex;未定义 → Unknown。
unsafe {
assert_eq!((s.get_int)(h, name.as_ptr(), 5, &mut v), BAD_INDEX);
}
let nope = cs("nope");
unsafe {
assert_eq!((s.get_int)(h, nope.as_ptr(), 0, &mut v), UNKNOWN);
}
// propReset:第 1 期明确不支持(无默认值快照)→ ErrUnsupported。
unsafe {
assert_eq!((s.reset)(h, name.as_ptr()), UNSUPPORTED);
}
// 空 handle → BadHandle。
unsafe {
assert_eq!(
(s.get_int)(std::ptr::null_mut(), name.as_ptr(), 0, &mut v),
BAD_HANDLE
);
}
}
/// memory suitealloc/free 正常;free(NULL) no-op;未知指针
/// free → BadHandle;零尺寸分配可往返。
///
/// 声明原含"实例销毁后账本无残留"sweep 是 pub(crate) 钩子
/// destroyInstance 路径),属 host 生命周期测试(lifecycle_test
/// ——此处只验 suite 契约。
#[test]
fn memory_suite_ledger() {
let s = memory_suite();
let mut a: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.alloc)(std::ptr::null_mut(), 256, &mut a), OK);
}
assert!(!a.is_null());
// 内存可用且对齐(f32 写读)。
unsafe {
let f = a as *mut f32;
*f = 1.5f32;
assert_eq!(*f, 1.5f32);
assert_eq!(a as usize % 16, 0);
}
// 释放;free(NULL) no-op;重复/未知 → BadHandle。
unsafe {
assert_eq!((s.free)(a), OK);
assert_eq!((s.free)(std::ptr::null_mut()), OK);
assert_eq!((s.free)(a), BAD_HANDLE);
}
// 零尺寸。
let mut z: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.alloc)(std::ptr::null_mut(), 0, &mut z), OK);
assert_eq!((s.free)(z), OK);
}
}
/// image effect suitedescribe 期 clipDefine/clipGetPropertySet 与
/// 属性读写;实例期 clipGetHandle。
///
/// clipGetImage/clipReleaseImage 配对依赖
/// [`oakplugin::clip::ClipInstance::fetch_image`]bridge::render 帧
/// 访问 C ABI 未冻结)——随最小测试插件落地补全(`// TODO(plugin)`)。
#[test]
fn image_effect_clip_image_pairing() {
let mut desc = EffectDescriptor::new();
let s = image_effect_suite();
let h = tag::make(&desc.props as *const PropertySet, tag::DESCRIPTOR);
// clipDefine + 属性读写(props 在偏移 0)。
let mut clip: *mut c_void = std::ptr::null_mut();
let name = cs("Source");
unsafe {
assert_eq!((s.clip_define)(h, name.as_ptr(), &mut clip), OK);
}
assert_eq!(tag::kind(clip), tag::CLIP);
let ps = property_suite();
let label_prop = cs("OfxPropLabel");
let optional_prop = cs("OfxImageClipPropOptional");
let label = cs("SourceLabel");
unsafe {
assert_eq!(
(ps.set_string)(clip, label_prop.as_ptr(), 0, label.as_ptr()),
OK
);
assert_eq!((ps.set_int)(clip, optional_prop.as_ptr(), 0, 1), OK);
}
let mut out: *mut c_char = std::ptr::null_mut();
unsafe {
assert_eq!((ps.get_string)(clip, label_prop.as_ptr(), 0, &mut out), OK);
assert_eq!(CStr::from_ptr(out).to_bytes(), b"SourceLabel");
}
// getPropertySet/getParamSet:返回 effect 本体。
let mut props: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.get_property_set)(h, &mut props), OK);
assert_eq!(props, h);
assert_eq!((s.get_param_set)(h, &mut props), OK);
assert_eq!(props, h);
}
// 实例期 clipGetHandle(手工构造实例 + clip 实例)。
let clip_desc = oakplugin::descriptor::ClipDescriptor {
props: PropertySet::new(),
name: "Source".into(),
};
let inst = std::sync::Arc::new(Instance {
props: PropertySet::new(),
plugin: dummy_plugin(EffectDescriptor::new()),
context: "OfxImageEffectContextFilter".into(),
params: ParamSetInstance { params: vec![] },
clips: vec![Box::new(oakplugin::clip::ClipInstance::from_descriptor(
&clip_desc,
))],
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(oakplugin::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
});
let ih = tag::make(&inst.props as *const PropertySet, tag::INSTANCE);
let mut clip_h: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!(
(s.clip_get_handle)(ih, name.as_ptr(), &mut clip_h, std::ptr::null_mut()),
OK
);
}
assert_eq!(tag::kind(clip_h), tag::CLIP);
// 未找到 → BadHandleHS:2067-2070)。
let nope = cs("Nope");
unsafe {
assert_eq!(
(s.clip_get_handle)(ih, nope.as_ptr(), &mut clip_h, std::ptr::null_mut()),
BAD_HANDLE
);
}
// clipGetImage/clipReleaseImage 配对:依赖 clip fetch_image
// bridge::render 未冻结)——插件落地后补全。
if common::test_plugin_dir().is_none() {
common::skip("clipGetImage 配对随最小测试插件落地(M11 §2.4)");
return;
}
// TODO(plugin):插件驱动 clipGetImage → clipReleaseImage 配对 +
// 不配对时的销毁记账断言。
}
/// param suitedescribe 期 define→getHandle→getValue(默认值);
/// 实例期 int/double/bool/choice/string/RGBA/2D/3D 的
/// setValue/getValue round-tripAtTime == 当前值。
///
/// 声明原含"paramSetValue 触发 instanceChanged":通知走
/// [`oakplugin::param::notify_instance_changed`]bridge 期实现)——
/// 随插件+桥落地补全(`// TODO(bridge)`)。
#[test]
fn param_suite_roundtrip_and_change_action() {
// describe 期。
let mut desc = EffectDescriptor::new();
let s = param_suite();
let dhandle = tag::make(&desc.props as *const PropertySet, tag::DESCRIPTOR);
let t = cs("OfxParamTypeDouble");
let n = cs("opacity");
let mut ph: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!(
(s.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut ph),
OK
);
}
assert_eq!(tag::kind(ph), tag::PARAM_DEF);
let mut v = 99.0;
unsafe {
assert_eq!((s.param_get_value)(ph, &mut v), OK);
}
assert_eq!(v, 0.0, "describe 期 getValue 应为默认值");
// describe 期 set → BadHandleHS verifyMagic 语义)。
unsafe {
assert_eq!((s.param_set_value)(ph, 42.0), BAD_HANDLE);
}
// paramGetHandle 按名取;未找到 → Unknown。
let mut ph2: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!(
(s.param_get_handle)(dhandle, n.as_ptr(), &mut ph2, std::ptr::null_mut()),
OK
);
assert_eq!(ph, ph2);
}
let nope = cs("nope");
unsafe {
assert_eq!(
(s.param_get_handle)(dhandle, nope.as_ptr(), &mut ph2, std::ptr::null_mut()),
UNKNOWN
);
}
// 实例期 round-trip。
let (_inst, ih) = make_instance();
let mut gain: *mut c_void = std::ptr::null_mut();
let mut pos: *mut c_void = std::ptr::null_mut();
let mut color: *mut c_void = std::ptr::null_mut();
let mut enabled: *mut c_void = std::ptr::null_mut();
let mut label: *mut c_void = std::ptr::null_mut();
unsafe {
for (n, out) in [
("gain", &mut gain),
("pos", &mut pos),
("color", &mut color),
("enabled", &mut enabled),
("label", &mut label),
] {
let n = cs(n);
assert_eq!(
(s.param_get_handle)(ih, n.as_ptr(), out, std::ptr::null_mut()),
OK
);
assert_eq!(tag::kind(*out), tag::PARAM_INSTANCE);
}
// Integer。
assert_eq!((s.param_set_value)(gain, 7), OK);
let mut iv = 0;
assert_eq!((s.param_get_value)(gain, &mut iv), OK);
assert_eq!(iv, 7);
// Double2D。
assert_eq!((s.param_set_value)(pos, 1.5, -2.5), OK);
let (mut x, mut y) = (0.0, 0.0);
assert_eq!((s.param_get_value)(pos, &mut x, &mut y), OK);
assert_eq!((x, y), (1.5, -2.5));
// RGB。
assert_eq!((s.param_set_value)(color, 0.1, 0.2, 0.3), OK);
let (mut r, mut g, mut b) = (0.0, 0.0, 0.0);
assert_eq!((s.param_get_value)(color, &mut r, &mut g, &mut b), OK);
assert_eq!((r, g, b), (0.1, 0.2, 0.3));
// Boolean。
assert_eq!((s.param_set_value)(enabled, 1), OK);
let mut e = 0;
assert_eq!((s.param_get_value)(enabled, &mut e), OK);
assert_eq!(e, 1);
// Stringget 返回内驻指针)。
let hello = cs("hello");
assert_eq!((s.param_set_value)(label, hello.as_ptr()), OK);
let mut p: *mut c_char = std::ptr::null_mut();
assert_eq!((s.param_get_value)(label, &mut p), OK);
assert_eq!(CStr::from_ptr(p).to_bytes(), b"hello");
// AtTime == 当前值(无动画)。
let mut v2 = 0.0;
assert_eq!((s.param_get_value_at_time)(gain, 12.0, &mut iv), OK);
assert_eq!(iv, 7);
assert_eq!((s.param_set_value_at_time)(gain, 12.0, 9), OK);
let mut iv2 = 0;
assert_eq!((s.param_get_value)(gain, &mut iv2), OK);
assert_eq!(iv2, 9);
}
// TODO(bridge)paramSetValue → instanceChanged 断言随
// notify_instance_changedbridge 期)落地。
}
/// paramGetValueAtTime/paramSetValueAtTime 与关键帧族
/// GetNumKeys/GetKeyTime/GetKeyIndex/DeleteKey/DeleteAllKeys):
/// 第 1 期无动画 → keys 恒 0、key 查询 BadIndex、删除 no-op OK。
#[test]
fn param_keyframe_family() {
let (_inst, ih) = make_instance();
let s = param_suite();
let mut opacity: *mut c_void = std::ptr::null_mut();
let name = cs("opacity");
unsafe {
assert_eq!(
(s.param_get_handle)(ih, name.as_ptr(), &mut opacity, std::ptr::null_mut()),
OK
);
}
let mut nkeys = -1;
unsafe {
assert_eq!((s.param_get_num_keys)(opacity, &mut nkeys), OK);
}
assert_eq!(nkeys, 0, "无动画支持:恒 0 个关键帧");
let mut kt = 0.0;
unsafe {
assert_eq!((s.param_get_key_time)(opacity, 0, &mut kt), BAD_INDEX);
}
let mut ki = 0;
unsafe {
assert_eq!((s.param_get_key_index)(opacity, 1.0, 0, &mut ki), BAD_INDEX);
// 删除与拷贝:无关键帧 → no-op OK。
assert_eq!((s.param_delete_key)(opacity, 1.0), OK);
assert_eq!((s.param_delete_all_keys)(opacity), OK);
assert_eq!(
(s.param_copy)(opacity, opacity, 1.0, 1.0, std::ptr::null()),
OK
);
// editBegin/End 括号。
assert_eq!((s.param_edit_begin)(opacity), OK);
assert_eq!((s.param_edit_end)(opacity), OK);
}
}
/// message suitev1 变长参数经 C shim 格式化后落到注册的捕获器
/// (%d/%s/%f 三种格式逐字一致);headless 默认(question 答"否")。
///
/// v2 的 va_list 入口无法从 Rust 构造调用(platform ABI),格式化
/// 路径与 v1 共用同一 C 函数(forward)——由测试插件从 C 侧覆盖
/// `// TODO(plugin)`)。
#[test]
fn message_suite_v1_v2() {
use oakplugin::suites::message::set_handler;
unsafe extern "C" fn capture(
type_: *const c_char,
message: *const c_char,
userdata: *mut 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(),
));
1
}
let mut captured: Vec<(String, String)> = Vec::new();
set_handler(Some(capture), &mut captured as *mut _ as *mut c_void);
let s = message_suite_v1();
let t = cs("OfxMessageError");
let id = cs("test-id");
let fmt = cs("v=%d s=%s f=%f");
let str_arg = cs("oak");
unsafe {
// 答复 YES → kOfxStatReplyYes12)。
assert_eq!(
(s.message)(
std::ptr::null_mut(),
t.as_ptr(),
id.as_ptr(),
fmt.as_ptr(),
42,
str_arg.as_ptr(),
1.5f64
),
12
);
}
assert_eq!(captured.len(), 1);
assert_eq!(captured[0].0, "OfxMessageError");
assert!(
captured[0].1.starts_with("v=42 s=oak f="),
"got {:?}",
captured[0].1
);
// NULL format → FailedC++ olivehost.cpp:267)。
unsafe {
assert_eq!(
(s.message)(
std::ptr::null_mut(),
t.as_ptr(),
id.as_ptr(),
std::ptr::null()
),
1
);
}
// 注销出口 → headless 默认:普通消息 OK。
set_handler(None, std::ptr::null_mut());
let msg = cs("ask");
unsafe {
assert_eq!(
(s.message)(std::ptr::null_mut(), t.as_ptr(), id.as_ptr(), msg.as_ptr()),
OK
);
}
// question 类型 + 无出口 → REPLY_NO13)。
let q = cs("OfxMessageQuestion");
unsafe {
assert_eq!(
(s.message)(
std::ptr::null_mut(),
q.as_ptr(),
id.as_ptr(),
fmt.as_ptr(),
1
),
13
);
}
}
/// progress suitestart/update/end 序列转发到 ProgressReporter
/// 回调返回 false 时 update 向插件返回取消状态。
#[test]
fn progress_suite_forwarding() {
use oakplugin::progress::ProgressReporter;
use oakplugin::suites::progress::set_current;
unsafe extern "C" fn capture_progress(p: f64, userdata: *mut c_void) -> c_int {
let v = unsafe { &mut *(userdata as *mut Vec<f64>) };
v.push(p);
0
}
unsafe extern "C" fn cancel_progress(_p: f64, _userdata: *mut c_void) -> c_int {
1
}
let s = progress_suite();
let mut seen: Vec<f64> = Vec::new();
set_current(Some(unsafe {
ProgressReporter::new(capture_progress, &mut seen as *mut _ as *mut c_void)
}));
unsafe {
assert_eq!((s.start)(std::ptr::null_mut(), std::ptr::null()), OK);
assert_eq!((s.update)(std::ptr::null_mut(), 0.25), OK);
assert_eq!((s.update)(std::ptr::null_mut(), 0.75), OK);
assert_eq!((s.end)(std::ptr::null_mut()), OK);
}
assert_eq!(seen, vec![0.25, 0.75]);
// 取消回调:update → REPLY_NO。
set_current(Some(unsafe {
ProgressReporter::new(cancel_progress, std::ptr::null_mut())
}));
unsafe {
assert_eq!((s.update)(std::ptr::null_mut(), 0.5), 13);
}
set_current(None);
}
/// timeline suitegetTime/getTimeBounds 返回渲染上下文注入的值;
/// 无上下文 → 0 / (0,0) 的 headless 默认。
#[test]
fn timeline_suite_values() {
use oakplugin::instance::{OfxRangeD, RenderScale};
use oakplugin::suites::{set_render_ctx, RenderCtx};
let s = timeline_suite();
let handle = 0x10usize as *mut c_void;
let mut t = 0.0;
let mut min = 0.0;
let mut max = 0.0;
// 无上下文。
unsafe {
assert_eq!((s.get_time)(handle, &mut t), OK);
assert_eq!(t, 0.0);
assert_eq!((s.get_time_bounds)(handle, &mut min, &mut max), OK);
assert_eq!((min, max), (0.0, 0.0));
}
// 注入上下文。
set_render_ctx(Some(RenderCtx {
time: 42.5,
scale: RenderScale { x: 1.0, y: 1.0 },
range: OfxRangeD {
min: 10.0,
max: 200.0,
},
}));
unsafe {
assert_eq!((s.get_time)(handle, &mut t), OK);
assert_eq!(t, 42.5);
assert_eq!((s.get_time_bounds)(handle, &mut min, &mut max), OK);
assert_eq!((min, max), (10.0, 200.0));
assert_eq!((s.goto_time)(handle, 99.0), OK);
}
set_render_ctx(None);
}
/// multithread suite:插件(此处以测试自身的工作线程模拟)起 8 线程
/// 各回调 property suite 一千次;无数据竞争、index/isSpawnedThread
/// 语义正确、join 完整。
#[test]
fn multithread_suite_spawned_callbacks() {
let s = multithread_suite();
let ps = property_suite();
// 共享属性集:每线程一个计数器属性,各写 1000 次。
// (测试自身即"插件":工作线程经 multiThread 起,回调 property
// suite——与插件场景同构。)
let set = std::sync::Arc::new(PropertySet::new());
for i in 0..8 {
let name = Box::leak(format!("ctr{i}").into_boxed_str());
set.set_one(name, Value::Int(0));
}
unsafe extern "C" fn counter_worker(index: c_uint, _count: c_uint, arg: *mut c_void) {
eprintln!("DBG worker {index} arg={:x}", arg as usize);
let set = unsafe { &*(arg as *const PropertySet) };
let name = Box::leak(format!("ctr{index}").into_boxed_str());
let mut v = 0;
unsafe {
let _ = (property_suite().get_int)(
set as *const _ as *mut c_void,
cs(name).as_ptr(),
0,
&mut v,
);
}
for _ in 0..1000 {
unsafe {
let _ = (property_suite().set_int)(
set as *const _ as *mut c_void,
cs(name).as_ptr(),
0,
v + 1,
);
let _ = (property_suite().get_int)(
set as *const _ as *mut c_void,
cs(name).as_ptr(),
0,
&mut v,
);
}
}
}
// 宿主线程身份。
let mut idx = 0;
let mut spawned = 1;
unsafe {
assert_eq!((s.index)(&mut idx), OK);
assert_eq!(idx, -1);
assert_eq!((s.is_spawned)(&mut spawned), OK);
assert_eq!(spawned, 0);
}
// 8 线程各 1000 次增量:最终值恰为 1000(无丢写 = 无竞争)。
// 注意传 `&*set`Arc 负载)而非 `&set`Arc 结构体)。
let raw: *const PropertySet = &*set;
unsafe {
assert_eq!((s.multi_thread)(counter_worker, 8, raw as *mut c_void), OK);
}
for i in 0..8 {
let name = Box::leak(format!("ctr{i}").into_boxed_str());
let mut v = 0;
// 注意 `&*set`Arc 负载)而非 `&set`Arc 结构体)。
unsafe {
assert_eq!(
(ps.get_int)(
&*set as *const _ as *mut c_void,
name.as_ptr() as *const c_char,
0,
&mut v
),
OK
);
}
assert_eq!(v, 1000, "线程 {i} 的计数应无丢失");
}
// 0 线程:no-op。
unsafe {
assert_eq!(
(s.multi_thread)(counter_worker, 0, std::ptr::null_mut()),
OK
);
}
}