|
1 | | -//! Regression tests for the studio's file-write pipeline: a debounced write |
| 1 | +//! Regression tests for the studio's file-write pipeline — a debounced write |
2 | 2 | //! that must rebase onto the current disk instead of replaying a stale |
3 | 3 | //! in-memory snapshot, coalesced edits inside one debounce window that must |
4 | 4 | //! all survive (not just the last), and undo/redo cancelling a still-pending |
5 | | -//! write before it can clobber the just-restored state. |
| 5 | +//! write before it can clobber the just-restored state — plus a render-thread |
| 6 | +//! panic that must surface as an error instead of being cached as an empty |
| 7 | +//! JPEG. |
6 | 8 | //! |
7 | 9 | //! `rustmotion-studio` has no `[dev-dependencies]` and cannot gain one in |
8 | 10 | //! this change, so every test below drives the crate's existing public |
9 | | -//! surface (`scenario::*`) against real temp files with plain synchronous |
10 | | -//! `#[test]`s — no Dioxus runtime, no async executor. That public surface is |
11 | | -//! itself the fix for the crate having no integration tests: the defects |
12 | | -//! lived in a debounce timer and Dioxus event handlers that cannot be driven |
13 | | -//! from a test, so each one was reduced to a pure decision over plain data |
14 | | -//! (`resolve_flush`, the pending-write queue) and the handler calls that |
15 | | -//! instead of deciding inline. |
| 11 | +//! surface (`scenario::*`, `editor::frames`) against real temp files with |
| 12 | +//! plain synchronous `#[test]`s — no Dioxus runtime, no async executor. That |
| 13 | +//! public surface is itself the fix for the crate having no integration |
| 14 | +//! tests: the defects lived in a debounce timer and Dioxus event handlers |
| 15 | +//! that cannot be driven from a test, so each one was reduced to a pure |
| 16 | +//! decision over plain data (`resolve_flush`, the pending-write queue, |
| 17 | +//! `render_frame_deep` returning `Result`) and the handler calls that instead |
| 18 | +//! of deciding inline. |
16 | 19 |
|
17 | 20 | use std::fs; |
18 | 21 | use std::path::PathBuf; |
19 | 22 | use std::sync::{Arc, Mutex}; |
20 | 23 |
|
| 24 | +use rustmotion_studio::editor::frames::render_frame_deep; |
21 | 25 | use rustmotion_studio::scenario::{ |
22 | 26 | apply_optimistic, empty_scenario, pending_write_slot, queue_mutation, record_edit, |
23 | 27 | resolve_flush, take_pending, undo, Mutation, Shared, SharedHistory, StudioModel, |
@@ -219,3 +223,46 @@ fn the_write_pipeline_round_trips_an_edit_through_a_real_file() { |
219 | 223 | ); |
220 | 224 | let _ = fs::remove_file(&path); |
221 | 225 | } |
| 226 | + |
| 227 | +// ── A render-thread panic surfaces as an error, never a cached empty JPEG ── |
| 228 | + |
| 229 | +#[test] |
| 230 | +fn a_render_thread_panic_is_reported_as_an_error_not_an_empty_jpeg() { |
| 231 | + let big = rustmotion::loader::load_scenario_from_source( |
| 232 | + None, |
| 233 | + Some(r##"{ "video": { "width": 64, "height": 64 }, "scenes": [ { "duration": 0.1 }, { "duration": 0.1 } ] }"##), |
| 234 | + ) |
| 235 | + .unwrap(); |
| 236 | + let tasks = rustmotion::encode::build_frame_tasks(&big); |
| 237 | + assert!( |
| 238 | + tasks.len() >= 2, |
| 239 | + "need frames spanning both scenes to reach scene_idx 1" |
| 240 | + ); |
| 241 | + |
| 242 | + // Deliberately mismatched: `tasks` reference a second scene this smaller |
| 243 | + // scenario does not have, which panics inside the render thread. |
| 244 | + let small = rustmotion::loader::load_scenario_from_source( |
| 245 | + None, |
| 246 | + Some(r##"{ "video": { "width": 64, "height": 64 }, "scenes": [ { "duration": 0.1 } ] }"##), |
| 247 | + ) |
| 248 | + .unwrap(); |
| 249 | + |
| 250 | + let last_frame = (tasks.len() - 1) as u32; |
| 251 | + let result = render_frame_deep(&small, &tasks, last_frame, 1.0); |
| 252 | + assert!( |
| 253 | + result.is_err(), |
| 254 | + "a scenario/task mismatch panics inside the render thread and must surface as Err" |
| 255 | + ); |
| 256 | +} |
| 257 | + |
| 258 | +#[test] |
| 259 | +fn a_normal_render_returns_nonempty_jpeg_bytes() { |
| 260 | + let scenario = rustmotion::loader::load_scenario_from_source( |
| 261 | + None, |
| 262 | + Some(r##"{ "video": { "width": 64, "height": 64 }, "scenes": [ { "duration": 0.1 } ] }"##), |
| 263 | + ) |
| 264 | + .unwrap(); |
| 265 | + let tasks = rustmotion::encode::build_frame_tasks(&scenario); |
| 266 | + let jpeg = render_frame_deep(&scenario, &tasks, 0, 1.0).expect("a normal render succeeds"); |
| 267 | + assert!(!jpeg.is_empty()); |
| 268 | +} |
0 commit comments