tests: replace placebo assertions with real behavior checks

This commit is contained in:
2026-09-09 16:33:35 +08:00
parent 5ab12b937f
commit 4f404f8cc6
9 changed files with 156 additions and 80 deletions
+2 -2
View File
@@ -688,8 +688,8 @@ fn node_behavior_defaults() {
assert!(b.value_hint_for_input("in").is_none());
assert_eq!(b.connected_render_output(&core, "in", -1), None);
let tr = TimeRange::new(Rational::new(0, 1), Rational::new(5, 1));
assert_eq!(b.input_time_adjustment("in", -1, tr, true), tr);
assert_eq!(b.output_time_adjustment("in", -1, tr, false), tr);
assert_eq!(b.input_time_adjustment(&core, "in", -1, tr, true), tr);
assert_eq!(b.output_time_adjustment(&core, "in", -1, tr, false), tr);
// value / process_samples / generate_frame no-ops.
let mut table = NodeValueTable::default();
+3 -4
View File
@@ -248,10 +248,9 @@ fn add_entry_reclaims_the_free_slot() {
let fresh = g.add_node(core, Box::new(TestNode { id: "fresh" }));
assert_ne!(fresh, victim, "the fresh node takes a different slot");
assert!(g.is_valid(victim), "the restored node survives add_node");
assert_eq!(
g.get(victim).map(|e| e.core.label.as_str()),
g.get(victim).map(|e| e.core.label.as_str()),
);
// The fresh node must not clobber the restored node's slot: the
// victim keeps its original (default empty) label.
assert_eq!(g.get(victim).map(|e| e.core.label.as_str()), Some(""));
assert_eq!(g.node_count(), count_before + 1);
}
+45
View File
@@ -319,6 +319,7 @@ fn connected_input_uses_adjusted_time() {
}
fn input_time_adjustment(
&self,
_core: &NodeCore,
input: &str,
_element: i32,
time: TimeRange,
@@ -353,3 +354,47 @@ fn connected_input_uses_adjusted_time() {
"the upstream was evaluated at the doubled time"
);
}
/// The real TimeOffsetNode (not a test double) remaps its upstream's
/// evaluation time through the trait: the keyframable offset lives on
/// the node core, which the trait method now receives. Evaluating the
/// offset at t=10 with `time_in` = 5 pulls the upstream at t=15 (C++
/// `get_remapped_time`: `input + time_in`).
#[test]
fn time_offset_node_shifts_upstream_evaluation_time() {
struct TimeEcho;
impl NodeBehavior for TimeEcho {
fn name(&self) -> &str {
"TimeEcho"
}
fn type_id(&self) -> &str {
"test.timeecho"
}
fn duplicate(&self, _c: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
Some(Box::new(TimeEcho))
}
fn value(&self, _c: &NodeCore, _i: &NodeValueRow, t: Rational, table: &mut NodeValueTable) {
table.push(ValueType::Rational, NodeValue::Rational(t), None);
}
}
let mut g = Graph::new();
let src = node_with_input(&mut g, Box::new(TimeEcho));
let (mut core, behavior) = oak_node::factory::Factory::global()
.create_any("org.olivevideoeditor.Olive.timeoffset")
.expect("timeoffset registered");
core.set_standard_value("time_in", -1, NodeValue::Rational(Rational::new(5, 1)));
let offset = g.add_node(core, behavior);
g.connect(src, offset, "input_in", -1).unwrap();
let mut t = Traverser::new();
let mut hooks = Noop;
let table = t
.evaluate(&g, &EvalRequest::new(offset, Rational::new(10, 1)), &mut hooks)
.unwrap();
assert_eq!(
table.get(ValueType::Rational),
Some(&NodeValue::Rational(Rational::new(15, 1))),
"the upstream must be evaluated at the offset time (10 + 5)"
);
}
+15 -13
View File
@@ -19,13 +19,13 @@
//! 快照在 0 期由**现行 C++ 实现**抓取入库(tests/ofx/snapshots/ 与
//! tests/ofx/frames/);本文件的测试断言 Rust 实现与之逐字段/逐像素
//! 一致。**0 期基建尚未落地**(tests/ofx/ 目录为空)——依赖快照/
//! 帧/GL 的用例一律 `#[ignore]`,落地后摘掉并补全断言(见
//! 帧的用例一律 `#[ignore]`,落地后摘掉并补全断言(见
//! [`descriptor_snapshots_match`] 的说明)。无真实 bundle 的环境
//! CI)整文件 skip。
//!
//! [`cimg_full_describe_smoke`] 不依赖快照:真实 CImg bundle 存在时
//! 全量 describe + 协商冒烟(健壮性,不比对),是本次唯一实际执行
//! 的用例
//! 本文件当前没有实际执行的用例:渲染/GL/色彩链路本身已实现并经
//! `gl_render_test.rs`/`colour_test.rs` 覆盖,这里缺的只是 golden
//! 参照物
mod common;
@@ -82,11 +82,11 @@ fn cimg_full_describe_smoke() {
///
/// # ignore 原因
///
/// `tests/ofx/frames/`C++ 实现抓取的 EXR + SHA256 库)尚未生成
/// 且 CPU 渲染链路依赖 renderer 桥(cargo 内无 liboakrender 真实现
/// 只有测试桩)。快照与真桥都落地后实现并摘除。
/// `tests/ofx/frames/`C++ 实现抓取的 EXR + SHA256 参照库)尚未生成
/// ——渲染链路本身已是真实现(render_driver + oak-render eval
/// 缺的只是 golden 参照物。参照库落地后实现并摘除。
#[test]
#[ignore = "M11 0 期渲染 goldentests/ofx/frames/+ 真 liboakrender 缺失;落地后实现并摘除"]
#[ignore = "M11 0 期渲染 golden 参照库tests/ofx/frames/尚未生成;落地后实现并摘除"]
fn render_golden_cpu_bitexact() {
todo!("0 期渲染 golden 落地后实现")
}
@@ -95,9 +95,11 @@ fn render_golden_cpu_bitexact() {
///
/// # ignore 原因
///
/// GL 路径属 M11 第 2 期(OpenGLRender suite 未实现),帧库亦未生成。
/// GL 渲染路径已实现(suites/gl_render.rsmacOS CGL + CPU 回退),
/// 缺的只是 golden 帧库(tests/ofx/frames/ 未生成)。帧库落地后实现
/// 并摘除。
#[test]
#[ignore = "GL 路径属 M11 第 2 期,且帧库未生成;2 期后实现并摘除"]
#[ignore = "golden 帧库(tests/ofx/frames/未生成;落地后实现并摘除"]
fn render_golden_gl_tolerant() {
todo!("M11 第 2 期 GL 路径落地后实现")
}
@@ -108,10 +110,10 @@ fn render_golden_gl_tolerant() {
///
/// # ignore 原因
///
/// 同 [`render_golden_cpu_bitexact`]依赖帧库与 OCIO 链路
/// ofxColour 属 M11 第 2 期)。
/// 同 [`render_golden_cpu_bitexact`]只缺 golden 参照帧库——F32
/// 管线和 OCIO/ofxColour 链路均已实现(colour_test.rs 覆盖)。
#[test]
#[ignore = "M11 0 期帧库 + 第 2 期 ofxColour/OCIO 链路缺失;落地后实现并摘除"]
#[ignore = "golden 帧库(tests/ofx/frames/)未生成;落地后实现并摘除"]
fn pipeline_is_f32_acescg() {
todo!("帧库与 OCIO 链路落地后实现")
}
+7 -2
View File
@@ -204,10 +204,15 @@ fn sequence_render_brackets() {
max: 200.0,
};
assert!(inst.value.begin_sequence_render(range).is_ok());
// begin 后实例记录该序列范围(timeline 上下文的 getTimeBounds
// 读它)。
let stored = *inst.value.sequence_range.lock().unwrap();
let stored = stored.expect("begin_sequence_render 必须登记范围");
assert_eq!((stored.min, stored.max), (10.0, 200.0));
assert!(inst.value.end_sequence_render(range).is_ok());
// end 后清除。
assert!(inst.value.sequence_range.lock().unwrap().is_none());
// begin → timeline 上下文带范围(经 render 设置;单测直达
// RenderCtx 的接线见 suites::timeline 测试)。
Host::global().shutdown();
});
}
+49 -15
View File
@@ -249,9 +249,8 @@ fn memory_suite_ledger() {
/// image effect suitedescribe 期 clipDefine/clipGetPropertySet 与
/// 属性读写;实例期 clipGetHandle。
///
/// clipGetImage/clipReleaseImage 配对依赖
/// [`oak_plugin::clip::ClipInstance::fetch_image`]bridge::render 帧
/// 访问 C ABI 未冻结)——随最小测试插件落地补全(`// TODO(plugin)`)。
/// clipGetImage/clipReleaseImage 配对
/// [`oak_plugin::clip::ClipInstance::fetch_image`] 与存活表记账验证。
#[test]
fn image_effect_clip_image_pairing() {
let mut desc = EffectDescriptor::new();
@@ -331,23 +330,57 @@ fn image_effect_clip_image_pairing() {
);
}
// clipGetImage/clipReleaseImage 配对:依赖 clip fetch_image
// bridge::render 未冻结)——插件落地后补全。
if common::test_plugin_dir().is_none() {
common::skip("clipGetImage 配对随最小测试插件落地(M11 §2.4)");
return;
// clipGetImage/clipReleaseImage 配对:输入 clip 排入一帧后 get
// 取到登记在存活表的图像;release 摘除;二次 release → BadHandle
// HS:2053-2068 的 releaseReference 配对)。
let mut frame = oak_render::eval::generate_frame(
oak_core::Rational::new(0, 1),
(2, 2),
oak_core::PixelFormat::F32,
)
.unwrap();
for (i, v) in [0.1f32, 0.2, 0.3, 1.0].iter().enumerate() {
frame.data[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
}
inst.clips[0].set_input_texture(
Some(oak_core::texture::Texture::wrap_frame(frame)),
0.0,
);
// 注意另取句柄:上面未找到路径的 clip_get_handle 把 clip_h 置空了。
let mut img_clip: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!(
(s.clip_get_handle)(ih, name.as_ptr(), &mut img_clip, std::ptr::null_mut()),
OK
);
}
let mut img: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!(
(s.clip_get_image)(img_clip, 0.0, std::ptr::null(), &mut img),
OK,
"clipGetImage 抓取排入的输入帧"
);
}
assert!(!img.is_null());
assert_eq!(tag::kind(img), tag::IMAGE);
unsafe {
assert_eq!((s.clip_release_image)(img), OK);
assert_eq!(
(s.clip_release_image)(img),
BAD_HANDLE,
"二次 release 必须 BadHandle(存活表已摘除)"
);
}
// TODO(plugin):插件驱动 clipGetImage → clipReleaseImage 配对 +
// 不配对时的销毁记账断言。
}
/// param suitedescribe 期 define→getHandle→getValue(默认值);
/// 实例期 int/double/bool/choice/string/RGBA/2D/3D 的
/// setValue/getValue round-tripAtTime == 当前值。
///
/// 声明含"paramSetValue 触发 instanceChanged":通知走
/// [`oak_plugin::param::notify_instance_changed`]bridge 期实现)——
/// 随插件+桥落地补全(`// TODO(bridge)`
/// 声明含"paramSetValue 触发 instanceChanged":通知走
/// [`oak_plugin::param::notify_instance_changed`],插件自改经 undo
/// 命令写回绑定节点的输入
#[test]
fn param_suite_roundtrip_and_change_action() {
// describe 期。
@@ -454,8 +487,9 @@ fn param_suite_roundtrip_and_change_action() {
assert_eq!(iv2, 9);
}
// TODO(bridge)paramSetValue → instanceChanged 断言随
// notify_instance_changedbridge 期)落地。
// paramSetValue → instanceChanged 回写(经 PARAM_OWNER 登记表,
// pub(crate))由 crate 内测试
// `suites::param::tests::set_value_writes_back_to_bound_node` 覆盖。
}
/// paramGetValueAtTime/paramSetValueAtTime 与关键帧族
+9 -14
View File
@@ -129,10 +129,12 @@ fn cancel_video_tasks_semantics() {
d.shutdown();
}
/// Change-record marshalling: every ChangeRecord kind survives the
/// C struct round-trip (layout pinned by the C ABI header).
/// Change-record layout: the C ABI header pins the struct to a 4-byte
/// kind + 48-byte opaque payload (52 bytes total). The kind constants
/// stay distinct so the wire tags never collide.
#[test]
fn change_record_marshalling() {
fn change_record_layout_is_abi_pinned() {
assert_eq!(std::mem::size_of::<oak_render::copier::ChangeRecord>(), 52);
let kinds = [
oak_render::copier::change_kind::NODE_ADD,
oak_render::copier::change_kind::NODE_REMOVE,
@@ -143,17 +145,10 @@ fn change_record_marshalling() {
oak_render::copier::change_kind::PROJECT_SETTING_CHANGE,
oak_render::copier::change_kind::FOOTAGE_PROXY,
];
for kind in kinds {
let record = oak_render::copier::ChangeRecord {
kind,
payload: [0xAA; 48],
};
assert_eq!(record.kind, kind);
assert_eq!(record.payload.len(), 48);
assert_eq!(
std::mem::size_of::<oak_render::copier::ChangeRecord>(),
52
);
for (i, a) in kinds.iter().enumerate() {
for b in &kinds[i + 1..] {
assert_ne!(a, b, "change-kind tags must be distinct");
}
}
}
@@ -243,9 +243,12 @@ fn snapshot_store_refcount() {
);
}
/// TimeRange sanity (used above).
/// The arena hands back the time a submitted ticket carries (the
/// metadata the playback clock queries while the job runs).
#[test]
fn range_sanity() {
let r = TimeRange::new(Rational::new(0, 1), Rational::new(10, 1));
assert_eq!(r.length(), Rational::new(10, 1));
fn arena_time_roundtrips_submitted_ticket() {
let (arena, d) = test_arena(ok_producer());
let id = arena.submit_video(params(Rational::new(7, 1)), Box::new(|_| {}));
assert_eq!(arena.time(id), Some(Rational::new(7, 1)));
d.shutdown();
}
+19 -26
View File
@@ -87,13 +87,27 @@ fn marker_set_time_point_preserves_name_color() {
assert_eq!(m.time().out(), Rational::new(40, 1));
}
/// `has_sibling_at_time` is a De-Qt simplification that always reports
/// `false`; sibling queries go through the list instead.
/// Sibling detection goes through the owning list: two markers sharing
/// an in point are both found by `get_marker_at_time` (the per-marker
/// `has_sibling_at_time` is a documented De-Qt simplification with no
/// parent pointer).
#[test]
fn marker_sibling_detection() {
let m = TimelineMarker::new();
assert!(!m.has_sibling_at_time(Rational::new(0, 1)));
assert!(!m.has_sibling_at_time(Rational::new(42, 1)));
let mut list = TimelineMarkerList::new();
let mut a = TimelineMarker::new();
a.set_time_point(Rational::new(42, 1));
a.set_name("a");
let mut b = TimelineMarker::new();
b.set_time_point(Rational::new(42, 1));
b.set_name("b");
let mut other = TimelineMarker::new();
other.set_time_point(Rational::new(7, 1));
list.add_marker(a);
list.add_marker(b);
list.add_marker(other);
assert_eq!(list.get_marker_at_time(Rational::new(42, 1)).map(|m| m.name()), Some("a"));
assert!(list.get_marker_at_time(Rational::new(7, 1)).is_some());
assert!(list.get_marker_at_time(Rational::new(8, 1)).is_none());
}
/// The list starts empty and grows with each `add_marker`, preserving
@@ -489,24 +503,3 @@ fn marker_commands_box_to_undo_command() {
assert_eq!(list_of(&list_h).size(), 1);
}
/// Loading a marker with a `color`/`in`/`out` attribute equal to the
/// sentinel matches how `EditToInfo` consumers treat defaults.
#[test]
fn marker_defaults_map_to_edit_to_info() {
// A default marker carries the null time and default color (0).
let m = TimelineMarker::new();
assert_eq!(m.color(), 0);
assert_eq!(m.time().in_(), Rational::new(0, 1));
// `EditToInfo` defaults mirror those sentinels: null node references
// (`None`, the single-lib replacement for the null `CHandle`) and the
// null rational for `nearest_time`.
let info = EditToInfo {
track: None,
nearest_time: m.time().in_(),
nearest_block: None,
};
assert!(info.track.is_none());
assert!(info.nearest_block.is_none());
assert_eq!(info.nearest_time, Rational::new(0, 1));
}