diff --git a/crates/oak-node/tests/engine_units_test.rs b/crates/oak-node/tests/engine_units_test.rs index 82c9be7e7..0e18b3b91 100644 --- a/crates/oak-node/tests/engine_units_test.rs +++ b/crates/oak-node/tests/engine_units_test.rs @@ -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(); diff --git a/crates/oak-node/tests/graph_test.rs b/crates/oak-node/tests/graph_test.rs index 8c8658d51..0a32a62e9 100644 --- a/crates/oak-node/tests/graph_test.rs +++ b/crates/oak-node/tests/graph_test.rs @@ -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); } diff --git a/crates/oak-node/tests/traverser_test.rs b/crates/oak-node/tests/traverser_test.rs index 8507aea34..3574a1fc6 100644 --- a/crates/oak-node/tests/traverser_test.rs +++ b/crates/oak-node/tests/traverser_test.rs @@ -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> { + 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)" + ); +} diff --git a/crates/oak-plugin/tests/golden_test.rs b/crates/oak-plugin/tests/golden_test.rs index 7e314b8d8..8d23d8403 100644 --- a/crates/oak-plugin/tests/golden_test.rs +++ b/crates/oak-plugin/tests/golden_test.rs @@ -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 期渲染 golden(tests/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.rs,macOS 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 链路落地后实现") } diff --git a/crates/oak-plugin/tests/negotiation_test.rs b/crates/oak-plugin/tests/negotiation_test.rs index 087b5bb58..43e25ea5b 100644 --- a/crates/oak-plugin/tests/negotiation_test.rs +++ b/crates/oak-plugin/tests/negotiation_test.rs @@ -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(); }); } diff --git a/crates/oak-plugin/tests/suites_test.rs b/crates/oak-plugin/tests/suites_test.rs index 614c50312..cd09d99a5 100644 --- a/crates/oak-plugin/tests/suites_test.rs +++ b/crates/oak-plugin/tests/suites_test.rs @@ -249,9 +249,8 @@ fn memory_suite_ledger() { /// image effect suite:describe 期 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 suite:describe 期 define→getHandle→getValue(默认值); /// 实例期 int/double/bool/choice/string/RGBA/2D/3D 的 /// setValue/getValue round-trip;AtTime == 当前值。 /// -/// 声明原含"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_changed(bridge 期)落地。 + // paramSetValue → instanceChanged 回写(经 PARAM_OWNER 登记表, + // pub(crate))由 crate 内测试 + // `suites::param::tests::set_value_writes_back_to_bound_node` 覆盖。 } /// paramGetValueAtTime/paramSetValueAtTime 与关键帧族 diff --git a/crates/oak-render/tests/copier_test.rs b/crates/oak-render/tests/copier_test.rs index 68099acaa..ba6b91f91 100644 --- a/crates/oak-render/tests/copier_test.rs +++ b/crates/oak-render/tests/copier_test.rs @@ -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::(), 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::(), - 52 - ); + for (i, a) in kinds.iter().enumerate() { + for b in &kinds[i + 1..] { + assert_ne!(a, b, "change-kind tags must be distinct"); + } } } diff --git a/crates/oak-render/tests/ticket_worker_test.rs b/crates/oak-render/tests/ticket_worker_test.rs index 1af25dbd4..f3ab4f246 100644 --- a/crates/oak-render/tests/ticket_worker_test.rs +++ b/crates/oak-render/tests/ticket_worker_test.rs @@ -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(); } diff --git a/crates/oak-timeline/tests/marker_test.rs b/crates/oak-timeline/tests/marker_test.rs index 490b86e1e..01f3b64b3 100644 --- a/crates/oak-timeline/tests/marker_test.rs +++ b/crates/oak-timeline/tests/marker_test.rs @@ -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)); -}