// 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 .
//! Interact 宿主端到端:生命周期 + action 调用面 + Draw suite 真实 GL。
//!
//! 链路:扫描最小测试插件的 interact 变体(org.oak.test-plugin.interact,
//! describe 期声明 kOfxImageEffectPluginPropOverlayInteractV2 → 指向
//! mainEntryInteract)→ `new_interact`(发 kOfxActionNewInteract)→
//! describe → create_instance → pen/key/idle 事件 → draw(宿主
//! gl_bridge 保持 GL current,插件经 Draw suite setColour + 原生 GL 画
//! 已知色块)→ 回读断言。
//!
//! 断言即"真实调用到插件"的证据:插件把每次 action 的实参写入
//! `OAK_TEST_PLUGIN_INTERACT_MARKER` 指向的标记文件(unix 下
//! `va_list` 由 stdarg.h 承担),测试逐行断言;GL 断言另读回 FBO 像素
//! 比对插件绘制颜色(与 CPU 路径可区分)。
//!
//! 门:事件用例不依赖 GL,任何平台可跑;draw 用例要求 macOS +
//! `OAK_GPU_TESTS`(GL 验收约定,CI 一律跳过)。
mod common;
use std::path::PathBuf;
use oakplugin::suites::{interact::Interact, status};
const INTERACT_PLUGIN_ID: &str = "org.oak.test-plugin.interact";
const BASE_PLUGIN_ID: &str = "org.oak.test-plugin";
/// 插件把 interact action 记录写入该环境变量指向的文件。
const MARKER_ENV: &str = "OAK_TEST_PLUGIN_INTERACT_MARKER";
/// 扫描测试插件 + 注册节点(幂等;不可用 → false = skip)。
fn scan_and_register() -> bool {
let Some(dir) = common::test_plugin_scan_dir() else {
common::skip("最小测试插件未构建");
return false;
};
if oakplugin::host::Host::global().cache.scan_path(&dir).is_err() {
common::skip("测试插件扫描失败");
return false;
}
oakplugin::node_factory::register_plugin_nodes();
true
}
/// 独立标记文件路径(临时目录,进程 id 防串)。
fn marker_path(tag: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"oak-interact-{}-{}.log",
std::process::id(),
tag
))
}
/// 读标记文件全部行。
fn read_marker(path: &PathBuf) -> Vec {
std::fs::read_to_string(path)
.unwrap_or_default()
.lines()
.map(|l| l.to_string())
.collect()
}
/// 无 interact 的插件:new_interact → None(main entry 对
/// kOfxActionNewInteract 返回 ReplyDefault 且未声明 overlay 入口)。
#[test]
fn base_plugin_has_no_interact() {
common::with_host(|| {
if !scan_and_register() {
return;
}
let inst = oakplugin::host::Host::global()
.create_instance(BASE_PLUGIN_ID, None)
.expect("base 插件实例应可建");
assert!(
inst.value.new_interact().is_none(),
"无 interact 的插件 new_interact 应为 None"
);
oakplugin::host::Host::global().shutdown();
});
}
/// 生命周期 + 事件调用面:new_interact → describe → create → pen/key/
/// idle → destroy。逐条断言插件侧真实记录与参数。
#[test]
fn interact_lifecycle_and_events() {
common::with_host(|| {
if !scan_and_register() {
return;
}
let marker = marker_path("events");
let _ = std::fs::remove_file(&marker);
unsafe { std::env::set_var(MARKER_ENV, &marker) };
let inst = oakplugin::host::Host::global()
.create_instance(INTERACT_PLUGIN_ID, None)
.expect("interact 变体实例应可建");
let interact_arc = inst
.value
.new_interact()
.expect("interact 变体应创建 interact");
let interact: &Interact = &interact_arc;
assert_eq!(interact.describe(), status::OK);
assert_eq!(interact.create_instance(), status::OK);
// 事件:坐标为视口像素;pixelScale 默认 1 → canonical == 视口。
assert_eq!(interact.pen_motion((10.0, 20.0), true, 5.0), status::OK);
assert_eq!(interact.pen_down((30.0, 40.0), 5.0), status::OK);
assert_eq!(interact.pen_up((30.0, 40.0), 5.0), status::OK);
assert_eq!(
interact.key_down(oakplugin::host::KEY_A, "a", 5.0),
status::OK
);
assert_eq!(interact.key_up(oakplugin::host::KEY_A, "a", 5.0), status::OK);
assert_eq!(interact.idle(), status::OK);
interact.destroy();
unsafe { std::env::remove_var(MARKER_ENV) };
let lines = read_marker(&marker);
let _ = std::fs::remove_file(&marker);
// 生命周期序列(按序)。
let seq = ["new_interact", "describe", "create", "destroy"];
let pos: Vec