// 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 .
//! 生命周期测试:实例创建/销毁配对、重复扫描、渲染中取消、
//! alive 泄漏断言。对应 M11 §3.5 验收线。
//!
//! 宿主单例无锁:全部用例经 [`common::with_host`] 串行化。
mod common;
use std::ffi::{c_char, c_int, c_void, CString};
use oakplugin::ffi::{
oakplugin_debug_alive_count, oakplugin_host_init, oakplugin_host_plugin_count,
oakplugin_host_scan, oakplugin_host_shutdown, oakplugin_instance_cancel,
oakplugin_instance_create, oakplugin_instance_free, oakplugin_instance_render,
oakplugin_instance_set_progress_cb,
};
use oakplugin::handle::CHandle;
const OK: i32 = 0;
const E_FAILED: i32 = -90003;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
const TEST_PLUGIN_ID: &str = "org.oak.test-plugin";
fn fake_texture(ctx: usize) -> CHandle {
CHandle {
ctx: ctx as *mut c_void,
addref: None,
release: None,
abi_version: 1,
}
}
/// 扫描并创建实例;不可用返回空句柄。
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(dirs.as_ptr(), 1) };
let id = cs(TEST_PLUGIN_ID);
unsafe { oakplugin_instance_create(id.as_ptr()) }
}
/// createInstance → destroyInstance 严格配对:alive 计数回到基线。
#[test]
fn instance_create_destroy_pairing() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let base = unsafe { oakplugin_debug_alive_count() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
assert_eq!(unsafe { oakplugin_debug_alive_count() }, base + 1);
unsafe { oakplugin_instance_free(&mut h) };
assert_eq!(
unsafe { oakplugin_debug_alive_count() },
base,
"destroy 后 alive 必须回基线"
);
unsafe { oakplugin_host_shutdown() };
});
}
/// 重复 scan 同一路径:插件列表不重复、顺序稳定(缓存去重语义,
/// HS: ofxhPluginCache.cpp)。
#[test]
fn rescan_is_idempotent() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
unsafe { oakplugin_host_shutdown() };
return;
}
let dir = cs(common::test_plugin_scan_dir().unwrap().to_str().unwrap());
let dirs = [dir.as_ptr()];
assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK);
let first = unsafe { oakplugin_host_plugin_count() };
assert!(first >= 1);
// 再扫两次:数量不变。
assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK);
assert_eq!(unsafe { oakplugin_host_scan(dirs.as_ptr(), 1) }, OK);
assert_eq!(
unsafe { oakplugin_host_plugin_count() },
first,
"重复扫描不得重复加载"
);
unsafe { oakplugin_host_shutdown() };
});
}
/// render 中途取消:进度回调返回非 0 后,插件的 progressUpdate 收到
/// 取消并中止 render;输出纹理不写半帧(测试插件在取消路径返回
/// Failed,宿主不落帧)。
#[cfg(feature = "test-stubs")]
#[test]
fn render_cancellation_is_atomic() {
common::with_host(|| {
use oakplugin::bridge::render::stub;
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
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()) };
// 预置输出帧并填 0xAA 哨兵:取消路径不得写半帧。
stub::setup_dst(64, 64, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let before = stub::dst_pixels();
assert!(!before.is_empty());
let dst = fake_texture(0xA1);
let r = unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) };
assert_eq!(r, E_FAILED, "取消应使 render 失败");
// 全有或全无:失败时不写。
assert_eq!(stub::dst_pixels(), before, "取消后输出不得被写");
unsafe { oakplugin_instance_free(&mut h) };
unsafe { oakplugin_host_shutdown() };
});
}
/// 压测:同一插件 256 次 create/render/destroy 循环后 alive 回到
/// 基线(当年内存问题的回归防线)。
#[cfg(feature = "test-stubs")]
#[test]
fn create_render_destroy_loop_no_leak() {
common::with_host(|| {
use oakplugin::bridge::render::stub;
unsafe { oakplugin_host_init() };
let base = unsafe { oakplugin_debug_alive_count() };
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
unsafe { oakplugin_host_shutdown() };
return;
}
stub::setup_dst(16, 16, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
for i in 0..256 {
let mut h = create_instance();
assert!(!h.is_null());
let r = unsafe { oakplugin_instance_render(h, dst, CHandle::null(), i as f64) };
assert_eq!(r, OK);
unsafe { oakplugin_instance_free(&mut h) };
assert_eq!(
unsafe { oakplugin_debug_alive_count() },
base,
"第 {i} 次循环后泄漏"
);
}
unsafe { oakplugin_host_shutdown() };
});
}
/// host_shutdown 时仍有活实例:宿主发 destroy 通知并卸载 bundle,
/// 之后该实例的任何 render 立即取消(不碰已卸载入口);再 create
/// 返回空句柄(缓存已清空——声明原期望 E_STATE,本实现的 create
/// 契约是"失败返回空句柄",以实例化为准)。
#[test]
fn shutdown_with_live_instances() {
common::with_host(|| {
unsafe { oakplugin_host_init() };
let mut h = create_instance();
if h.is_null() {
unsafe { oakplugin_host_shutdown() };
return;
}
// 带活实例 shutdown:不崩;实例被打取消标记。
unsafe { oakplugin_host_shutdown() };
// C 侧句柄仍持 Arc:实例内存存活,但 render 必须失败。
let dst = fake_texture(0xA1);
assert_eq!(
unsafe { oakplugin_instance_render(h, dst, CHandle::null(), 0.0) },
E_FAILED
);
unsafe { oakplugin_instance_free(&mut h) };
assert_eq!(
unsafe { oakplugin_debug_alive_count() },
0,
"句柄释放后归零"
);
// 之后再 create:插件已卸载 → 空句柄。
let id = cs(TEST_PLUGIN_ID);
let h2 = unsafe { oakplugin_instance_create(id.as_ptr()) };
assert!(h2.is_null());
});
}
/// 多实例并发:同一插件 16 实例并发 render(宿主侧不串——插件
/// 全局状态由插件自保)。
#[cfg(feature = "test-stubs")]
#[test]
fn concurrent_instances_render() {
common::with_host(|| {
use oakplugin::bridge::render::stub;
unsafe { oakplugin_host_init() };
if common::test_plugin_scan_dir().is_none() {
common::skip("最小测试插件未构建");
unsafe { oakplugin_host_shutdown() };
return;
}
// 16 个实例(句柄数组;经线程并发 render)。
let mut handles: Vec = (0..16).map(|_| create_instance()).collect();
assert!(handles.iter().all(|h| !h.is_null()));
stub::setup_dst(32, 32, oakplugin::bridge::render::PIXEL_FORMAT_F32);
let dst = fake_texture(0xA1);
let threads: Vec<_> = handles
.iter()
.enumerate()
.map(|(i, h)| {
let h = *h;
std::thread::spawn(move || unsafe {
oakplugin_instance_render(h, dst, CHandle::null(), i as f64)
})
})
.collect();
for t in threads {
assert_eq!(t.join().unwrap(), OK);
}
for h in handles.iter_mut() {
unsafe { oakplugin_instance_free(h) };
}
assert_eq!(unsafe { oakplugin_debug_alive_count() }, 0);
unsafe { oakplugin_host_shutdown() };
});
}