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

204 lines
6.3 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/>.
//! 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` 未实现 PartialEqPointer 无法比较),测试用 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 返回 Nonedimension 为 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);
// 缺失属性 removeno-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())
}