- 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)
204 lines
6.3 KiB
Rust
204 lines
6.3 KiB
Rust
// 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/>.
|
||
|
||
//! property.rs 的契约测试:OFX 属性集语义。
|
||
//!
|
||
//! 参照:HS: ofxhProperty.cpp。语义要点:多维数组、越界/缺失/
|
||
/// 类型不符的行为、define 替换语义。
|
||
mod common;
|
||
|
||
use std::ffi::{c_void, CString};
|
||
use std::sync::Arc;
|
||
|
||
use oakplugin::error::Error;
|
||
use oakplugin::property::{PropertySet, Value};
|
||
|
||
/// `Value` 未实现 PartialEq(Pointer 无法比较),测试用 Debug 串
|
||
/// 比较做值相等断言(同进程内指针的 Debug 输出确定)。
|
||
fn val_eq(a: &Value, b: &Value) -> bool {
|
||
format!("{a:?}") == format!("{b:?}")
|
||
}
|
||
|
||
/// define 后 get 命中;同名 define 整体替换值数组(维度随之变化)。
|
||
#[test]
|
||
fn define_and_replace() {
|
||
let s = PropertySet::new();
|
||
s.define("a", vec![Value::Int(1), Value::Int(2)]);
|
||
assert_eq!(s.dimension("a"), 2);
|
||
assert!(val_eq(&s.get("a", 0).unwrap(), &Value::Int(1)));
|
||
|
||
// 同名整体替换:旧值数组连同维度一起被覆盖。
|
||
s.define("a", vec![Value::Int(9)]);
|
||
assert_eq!(s.dimension("a"), 1);
|
||
assert!(val_eq(&s.get("a", 0).unwrap(), &Value::Int(9)));
|
||
assert!(s.get("a", 1).is_none());
|
||
}
|
||
|
||
/// 未定义属性的 get 返回 None;dimension 为 0。
|
||
#[test]
|
||
fn missing_property() {
|
||
let s = PropertySet::new();
|
||
assert!(s.get("nope", 0).is_none());
|
||
assert!(s.get("nope", 42).is_none());
|
||
assert_eq!(s.dimension("nope"), 0);
|
||
}
|
||
|
||
/// 越界读取(index >= dimension)返回 None。
|
||
#[test]
|
||
fn out_of_bounds_read() {
|
||
let s = PropertySet::new();
|
||
s.define("a", vec![Value::Int(1)]);
|
||
assert!(s.get("a", 1).is_none());
|
||
assert!(s.get("a", 100).is_none());
|
||
}
|
||
|
||
/// set_at 覆盖指定下标且不改维度;对缺失属性返回 NotFound 错误。
|
||
#[test]
|
||
fn set_at_semantics() {
|
||
let s = PropertySet::new();
|
||
s.define("a", vec![Value::Int(1), Value::Int(2)]);
|
||
|
||
// 覆盖命中下标,维度不变。
|
||
assert!(s.set_at("a", 0, Value::Int(9)).is_ok());
|
||
assert!(val_eq(&s.get("a", 0).unwrap(), &Value::Int(9)));
|
||
assert_eq!(s.dimension("a"), 2);
|
||
|
||
// 越界写:维度固定语义(不自动扩容)→ NotFound。
|
||
assert!(matches!(
|
||
s.set_at("a", 2, Value::Int(3)),
|
||
Err(Error::NotFound)
|
||
));
|
||
assert_eq!(s.dimension("a"), 2);
|
||
|
||
// 缺失属性:NotFound。
|
||
assert!(matches!(
|
||
s.set_at("b", 0, Value::Int(1)),
|
||
Err(Error::NotFound)
|
||
));
|
||
}
|
||
|
||
/// 四种值类型(Int/Double/String/Pointer)各自的存取 round-trip;
|
||
/// 字符串含空串与 UTF-8 非 ASCII。
|
||
#[test]
|
||
fn typed_roundtrip() {
|
||
let s = PropertySet::new();
|
||
let non_ascii = CString::new("你好,wörld ✓").unwrap();
|
||
let raw = 0x1234usize as *mut c_void;
|
||
|
||
s.define("ints", vec![Value::Int(-7)]);
|
||
s.define("dbls", vec![Value::Double(1.5)]);
|
||
s.define("strs", vec![Value::String(non_ascii.clone())]);
|
||
s.define("ptrs", vec![Value::Pointer(raw)]);
|
||
|
||
assert!(val_eq(&s.get("ints", 0).unwrap(), &Value::Int(-7)));
|
||
assert!(val_eq(&s.get("dbls", 0).unwrap(), &Value::Double(1.5)));
|
||
assert!(val_eq(
|
||
&s.get("strs", 0).unwrap(),
|
||
&Value::String(non_ascii)
|
||
));
|
||
assert!(val_eq(&s.get("ptrs", 0).unwrap(), &Value::Pointer(raw)));
|
||
|
||
// 空串也是合法属性值。
|
||
s.set_one("empty", Value::String(CString::new("").unwrap()));
|
||
assert!(val_eq(
|
||
&s.get("empty", 0).unwrap(),
|
||
&Value::String(CString::new("").unwrap())
|
||
));
|
||
}
|
||
|
||
/// remove 后属性消失;remove 缺失属性 no-op。
|
||
#[test]
|
||
fn remove_semantics() {
|
||
let s = PropertySet::new();
|
||
s.set_one("a", Value::Int(1));
|
||
s.remove("a");
|
||
assert!(s.get("a", 0).is_none());
|
||
assert_eq!(s.dimension("a"), 0);
|
||
|
||
// 缺失属性 remove:no-op,不崩。
|
||
s.remove("a");
|
||
s.remove("never-existed");
|
||
}
|
||
|
||
/// snapshot 返回全量快照且与后续修改隔离(深拷贝)。
|
||
#[test]
|
||
fn snapshot_is_isolated() {
|
||
let s = PropertySet::new();
|
||
s.define("a", vec![Value::Int(1)]);
|
||
|
||
let snap = s.snapshot();
|
||
assert_eq!(snap.len(), 1);
|
||
assert_eq!(snap[0].name, "a");
|
||
|
||
// 后续对属性集的修改不影响快照。
|
||
s.set_at("a", 0, Value::Int(2)).unwrap();
|
||
s.set_one("b", Value::Int(3));
|
||
assert_eq!(snap.len(), 1);
|
||
assert!(val_eq(&snap[0].values[0], &Value::Int(1)));
|
||
|
||
// 反向隔离:改快照不回写属性集(深拷贝验证)。
|
||
let mut snap = snap;
|
||
snap[0].values[0] = Value::Int(99);
|
||
assert!(val_eq(&s.get("a", 0).unwrap(), &Value::Int(2)));
|
||
}
|
||
|
||
/// 并发读写(32 线程 define/get/set_at 交错)不崩、数据自洽
|
||
/// (属性集被所有 suite 共享,必须线程安全)。
|
||
#[test]
|
||
fn concurrent_access() {
|
||
// 线程要求 'static:经 Arc 共享属性集(对应插件线程经句柄共享)。
|
||
let s = Arc::new(PropertySet::new());
|
||
let names: Vec<&'static str> = (0..32).map(leak_name).collect();
|
||
for (i, name) in names.iter().enumerate() {
|
||
s.set_one(name, Value::Int(i as i32));
|
||
}
|
||
|
||
let threads: Vec<_> = (0..32usize)
|
||
.map(|i| {
|
||
let s = Arc::clone(&s);
|
||
// &'static str 是 Copy:拷贝进闭包,线程各自持名。
|
||
let name = names[i];
|
||
std::thread::spawn(move || {
|
||
for k in 0..500 {
|
||
// 读永远合法(属性始终存在、维度恒为 1)。
|
||
assert!(s.get(name, 0).is_some());
|
||
if i % 2 == 0 {
|
||
// 偶数线程:set_at 覆盖(不改维度)。
|
||
s.set_at(name, 0, Value::Int((i + k) as i32)).unwrap();
|
||
} else {
|
||
// 奇数线程:define 整体替换(维度同样恒为 1)。
|
||
s.define(name, vec![Value::Int((i as i32) - k as i32)]);
|
||
}
|
||
}
|
||
})
|
||
})
|
||
.collect();
|
||
for t in threads {
|
||
t.join().unwrap();
|
||
}
|
||
|
||
// 数据自洽:32 个属性全部存活、维度未被撑大。
|
||
for (i, name) in names.iter().enumerate() {
|
||
assert_eq!(s.dimension(name), 1);
|
||
assert!(s.get(name, 0).is_some());
|
||
}
|
||
}
|
||
|
||
/// 泄露一个 `&'static str` 属性名(并发测试用;测试进程结束即回收)。
|
||
fn leak_name(i: usize) -> &'static str {
|
||
Box::leak(format!("p{i}").into_boxed_str())
|
||
}
|