From 818897b74e7addbf5811bb1c67ed2494a6bd2308 Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Fri, 27 Jan 2023 18:49:45 +0100 Subject: [PATCH] tests: Move run_swf to util/runner --- tests/tests/regression_tests.rs | 203 ++------------------------------ tests/tests/util/mod.rs | 1 + tests/tests/util/runner.rs | 191 ++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 191 deletions(-) create mode 100644 tests/tests/util/runner.rs diff --git a/tests/tests/regression_tests.rs b/tests/tests/regression_tests.rs index 400ae34be..f414178f9 100644 --- a/tests/tests/regression_tests.rs +++ b/tests/tests/regression_tests.rs @@ -5,20 +5,17 @@ use regex::Regex; use ruffle_core::backend::{ log::LogBackend, - navigator::{NullExecutor, NullNavigatorBackend}, storage::{MemoryStorageBackend, StorageBackend}, }; use ruffle_core::context::UpdateContext; -use ruffle_core::events::MouseButton as RuffleMouseButton; use ruffle_core::external::Value as ExternalValue; use ruffle_core::external::{ExternalInterfaceMethod, ExternalInterfaceProvider}; -use ruffle_core::limits::ExecutionLimit; -use ruffle_core::tag_utils::SwfMovie; -use ruffle_core::{Player, PlayerBuilder, PlayerEvent}; -use ruffle_input_format::{AutomatedEvent, InputInjector, MouseButton as InputMouseButton}; +use ruffle_core::Player; use anyhow::Context; +use anyhow::Result; use libtest_mimic::{Arguments, Trial}; +use ruffle_input_format::InputInjector; #[cfg(feature = "imgtests")] use ruffle_render_wgpu::backend::WgpuRenderBackend; #[cfg(feature = "imgtests")] @@ -28,7 +25,7 @@ use std::collections::BTreeMap; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use std::time::Duration; +use util::runner::run_swf; use util::test::Test; mod util; @@ -42,9 +39,7 @@ fn set_logger() { .try_init(); } -type Error = Box; - -fn external_interface_avm1() -> Result<(), Error> { +fn external_interface_avm1() -> Result<()> { set_logger(); test_swf_with_hooks( Path::new("tests/swfs/avm1/external_interface/test.swf"), @@ -98,7 +93,7 @@ fn external_interface_avm1() -> Result<(), Error> { ) } -fn external_interface_avm2() -> Result<(), Error> { +fn external_interface_avm2() -> Result<()> { set_logger(); test_swf_with_hooks( Path::new("tests/swfs/avm2/external_interface/test.swf"), @@ -143,7 +138,7 @@ fn external_interface_avm2() -> Result<(), Error> { ) } -fn shared_object_avm1() -> Result<(), Error> { +fn shared_object_avm1() -> Result<()> { set_logger(); // Test SharedObject persistence. Run an SWF that saves data // to a shared object twice and verify that the data is saved. @@ -196,7 +191,7 @@ fn shared_object_avm1() -> Result<(), Error> { Ok(()) } -fn shared_object_avm2() -> Result<(), Error> { +fn shared_object_avm2() -> Result<()> { set_logger(); // Test SharedObject persistence. Run an SWF that saves data // to a shared object twice and verify that the data is saved. @@ -257,11 +252,11 @@ fn test_swf_with_hooks( num_frames: u32, simulated_input_path: &Path, expected_output_path: &Path, - before_start: impl FnOnce(Arc>) -> Result<(), Error>, - before_end: impl FnOnce(Arc>) -> Result<(), Error>, + before_start: impl FnOnce(Arc>) -> Result<()>, + before_end: impl FnOnce(Arc>) -> Result<()>, check_img: bool, frame_time_sleep: bool, -) -> Result<(), Error> { +) -> Result<()> { let injector = InputInjector::from_file(simulated_input_path).unwrap_or_else(|_| InputInjector::empty()); let mut expected_output = std::fs::read_to_string(expected_output_path)?.replace("\r\n", "\n"); @@ -299,7 +294,7 @@ fn test_swf_approx( num_patterns: &[Regex], check_img: bool, approx_assert_fn: impl Fn(f64, f64), -) -> Result<(), Error> { +) -> Result<()> { let injector = InputInjector::from_file(simulated_input_path).unwrap_or_else(|_| InputInjector::empty()); let trace_log = run_swf( @@ -391,180 +386,6 @@ fn test_swf_approx( Ok(()) } -/// Loads an SWF and runs it through the Ruffle core for a number of frames. -/// Tests that the trace output matches the given expected output. -fn run_swf( - swf_path: &Path, - num_frames: u32, - before_start: impl FnOnce(Arc>) -> Result<(), Error>, - mut injector: InputInjector, - before_end: impl FnOnce(Arc>) -> Result<(), Error>, - #[allow(unused)] mut check_img: bool, - frame_time_sleep: bool, -) -> Result { - #[allow(unused_assignments)] - { - check_img &= RUN_IMG_TESTS; - } - - let base_path = Path::new(swf_path).parent().unwrap(); - let mut executor = NullExecutor::new(); - let movie = SwfMovie::from_path(swf_path, None)?; - let frame_time = 1000.0 / movie.frame_rate().to_f64(); - let frame_time_duration = Duration::from_millis(frame_time as u64); - let trace_output = Rc::new(RefCell::new(Vec::new())); - - #[allow(unused_mut)] - let mut builder = PlayerBuilder::new(); - - #[cfg(feature = "imgtests")] - if check_img { - const BACKEND: wgpu::Backends = wgpu::Backends::PRIMARY; - - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: BACKEND, - dx12_shader_compiler: wgpu::Dx12Compiler::default(), - }); - - let descriptors = - futures::executor::block_on(WgpuRenderBackend::::build_descriptors( - BACKEND, - instance, - None, - Default::default(), - None, - ))?; - - let width = movie.width().to_pixels() as u32; - let height = movie.height().to_pixels() as u32; - - let target = TextureTarget::new(&descriptors.device, (width, height))?; - - builder = builder - .with_renderer(WgpuRenderBackend::new(Arc::new(descriptors), target, 4)?) - .with_viewport_dimensions(width, height, 1.0); - }; - - let player = builder - .with_log(TestLogBackend::new(trace_output.clone())) - .with_navigator(NullNavigatorBackend::with_base_path(base_path, &executor)?) - .with_max_execution_duration(Duration::from_secs(300)) - .with_viewport_dimensions( - movie.width().to_pixels() as u32, - movie.height().to_pixels() as u32, - 1.0, - ) - .with_movie(movie) - .build(); - - before_start(player.clone())?; - - for _ in 0..num_frames { - // If requested, ensure that the 'expected' amount of - // time actually elapses between frames. This is useful for - // tests that call 'flash.utils.getTimer()' and use - // 'setInterval'/'flash.utils.Timer' - // - // Note that when Ruffle actually runs frames, we can - // execute frames faster than this in order to 'catch up' - // if we've fallen behind. However, in order to make regression - // tests deterministic, we always call 'update_timers' with - // an elapsed time of 'frame_time'. By sleeping for 'frame_time_duration', - // we ensure that the result of 'flash.utils.getTimer()' is consistent - // with timer execution (timers will see an elapsed time of *at least* - // the requested timer interval). - if frame_time_sleep { - std::thread::sleep(frame_time_duration); - } - - while !player - .lock() - .unwrap() - .preload(&mut ExecutionLimit::exhausted()) - {} - - player.lock().unwrap().run_frame(); - player.lock().unwrap().update_timers(frame_time); - executor.run(); - - injector.next(|evt, _btns_down| { - player.lock().unwrap().handle_event(match evt { - AutomatedEvent::MouseDown { pos, btn } => PlayerEvent::MouseDown { - x: pos.0, - y: pos.1, - button: match btn { - InputMouseButton::Left => RuffleMouseButton::Left, - InputMouseButton::Middle => RuffleMouseButton::Middle, - InputMouseButton::Right => RuffleMouseButton::Right, - }, - }, - AutomatedEvent::MouseMove { pos } => PlayerEvent::MouseMove { x: pos.0, y: pos.1 }, - AutomatedEvent::MouseUp { pos, btn } => PlayerEvent::MouseUp { - x: pos.0, - y: pos.1, - button: match btn { - InputMouseButton::Left => RuffleMouseButton::Left, - InputMouseButton::Middle => RuffleMouseButton::Middle, - InputMouseButton::Right => RuffleMouseButton::Right, - }, - }, - AutomatedEvent::Wait => unreachable!(), - }); - }); - // Rendering has side-effects (such as processing 'DisplayObject.scrollRect' updates) - player.lock().unwrap().render(); - } - - // Render the image to disk - // FIXME: Determine how we want to compare against on on-disk image - #[cfg(feature = "imgtests")] - if check_img { - let mut player_lock = player.lock().unwrap(); - player_lock.render(); - let renderer = player_lock - .renderer_mut() - .downcast_mut::>() - .unwrap(); - - // Use straight alpha, since we want to save this as a PNG - let actual_image = renderer - .capture_frame(false) - .expect("Failed to capture image"); - - let info = renderer.descriptors().adapter.get_info(); - let suffix = format!("{}-{:?}", std::env::consts::OS, info.backend); - - let expected_image_path = base_path.join(format!("expected-{}.png", &suffix)); - let expected_image = image::open(&expected_image_path); - - let matches = match expected_image { - Ok(img) => { - img.as_rgba8().expect("Expected 8-bit RGBA image").as_raw() == actual_image.as_raw() - } - Err(e) => { - eprintln!( - "Failed to open expected image {:?}: {e:?}", - &expected_image_path - ); - false - } - }; - - if !matches { - let actual_image_path = base_path.join(format!("actual-{suffix}.png")); - actual_image.save_with_format(&actual_image_path, image::ImageFormat::Png)?; - panic!("Test output does not match expected image - saved actual image to {actual_image_path:?}"); - } - } - - before_end(player)?; - - executor.run(); - - let trace = trace_output.borrow().join("\n"); - Ok(trace) -} - struct TestLogBackend { trace_output: Rc>>, } diff --git a/tests/tests/util/mod.rs b/tests/tests/util/mod.rs index 9a2931e38..c7c703a71 100644 --- a/tests/tests/util/mod.rs +++ b/tests/tests/util/mod.rs @@ -2,6 +2,7 @@ // https://doc.rust-lang.org/book/ch11-03-test-organization.html pub mod options; +pub mod runner; pub mod test; /// Wrapper around string slice that makes debug output `{:?}` to print string same way as `{}`. diff --git a/tests/tests/util/runner.rs b/tests/tests/util/runner.rs new file mode 100644 index 000000000..755d03ecb --- /dev/null +++ b/tests/tests/util/runner.rs @@ -0,0 +1,191 @@ +use crate::{TestLogBackend, RUN_IMG_TESTS}; +use anyhow::{anyhow, Result}; +use ruffle_core::backend::navigator::{NullExecutor, NullNavigatorBackend}; +use ruffle_core::events::MouseButton as RuffleMouseButton; +use ruffle_core::limits::ExecutionLimit; +use ruffle_core::tag_utils::SwfMovie; +use ruffle_core::{Player, PlayerBuilder, PlayerEvent}; +use ruffle_input_format::{AutomatedEvent, InputInjector, MouseButton as InputMouseButton}; +#[cfg(feature = "imgtests")] +use ruffle_render_wgpu::backend::WgpuRenderBackend; +#[cfg(feature = "imgtests")] +use ruffle_render_wgpu::{target::TextureTarget, wgpu}; +use std::cell::RefCell; +use std::path::Path; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +/// Loads an SWF and runs it through the Ruffle core for a number of frames. +/// Tests that the trace output matches the given expected output. +pub fn run_swf( + swf_path: &Path, + num_frames: u32, + before_start: impl FnOnce(Arc>) -> Result<()>, + mut injector: InputInjector, + before_end: impl FnOnce(Arc>) -> Result<()>, + #[allow(unused)] mut check_img: bool, + frame_time_sleep: bool, +) -> Result { + #[allow(unused_assignments)] + { + check_img &= RUN_IMG_TESTS; + } + + let base_path = Path::new(swf_path).parent().unwrap(); + let mut executor = NullExecutor::new(); + let movie = SwfMovie::from_path(swf_path, None).map_err(|e| anyhow!(e.to_string()))?; + let frame_time = 1000.0 / movie.frame_rate().to_f64(); + let frame_time_duration = Duration::from_millis(frame_time as u64); + let trace_output = Rc::new(RefCell::new(Vec::new())); + + #[allow(unused_mut)] + let mut builder = PlayerBuilder::new(); + + #[cfg(feature = "imgtests")] + if check_img { + const BACKEND: wgpu::Backends = wgpu::Backends::PRIMARY; + + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: BACKEND, + dx12_shader_compiler: wgpu::Dx12Compiler::default(), + }); + + let descriptors = + futures::executor::block_on(WgpuRenderBackend::::build_descriptors( + BACKEND, + instance, + None, + Default::default(), + None, + ))?; + + let width = movie.width().to_pixels() as u32; + let height = movie.height().to_pixels() as u32; + + let target = TextureTarget::new(&descriptors.device, (width, height))?; + + builder = builder + .with_renderer(WgpuRenderBackend::new(Arc::new(descriptors), target, 4)?) + .with_viewport_dimensions(width, height, 1.0); + }; + + let player = builder + .with_log(TestLogBackend::new(trace_output.clone())) + .with_navigator(NullNavigatorBackend::with_base_path(base_path, &executor)?) + .with_max_execution_duration(Duration::from_secs(300)) + .with_viewport_dimensions( + movie.width().to_pixels() as u32, + movie.height().to_pixels() as u32, + 1.0, + ) + .with_movie(movie) + .build(); + + before_start(player.clone())?; + + for _ in 0..num_frames { + // If requested, ensure that the 'expected' amount of + // time actually elapses between frames. This is useful for + // tests that call 'flash.utils.getTimer()' and use + // 'setInterval'/'flash.utils.Timer' + // + // Note that when Ruffle actually runs frames, we can + // execute frames faster than this in order to 'catch up' + // if we've fallen behind. However, in order to make regression + // tests deterministic, we always call 'update_timers' with + // an elapsed time of 'frame_time'. By sleeping for 'frame_time_duration', + // we ensure that the result of 'flash.utils.getTimer()' is consistent + // with timer execution (timers will see an elapsed time of *at least* + // the requested timer interval). + if frame_time_sleep { + std::thread::sleep(frame_time_duration); + } + + while !player + .lock() + .unwrap() + .preload(&mut ExecutionLimit::exhausted()) + {} + + player.lock().unwrap().run_frame(); + player.lock().unwrap().update_timers(frame_time); + executor.run(); + + injector.next(|evt, _btns_down| { + player.lock().unwrap().handle_event(match evt { + AutomatedEvent::MouseDown { pos, btn } => PlayerEvent::MouseDown { + x: pos.0, + y: pos.1, + button: match btn { + InputMouseButton::Left => RuffleMouseButton::Left, + InputMouseButton::Middle => RuffleMouseButton::Middle, + InputMouseButton::Right => RuffleMouseButton::Right, + }, + }, + AutomatedEvent::MouseMove { pos } => PlayerEvent::MouseMove { x: pos.0, y: pos.1 }, + AutomatedEvent::MouseUp { pos, btn } => PlayerEvent::MouseUp { + x: pos.0, + y: pos.1, + button: match btn { + InputMouseButton::Left => RuffleMouseButton::Left, + InputMouseButton::Middle => RuffleMouseButton::Middle, + InputMouseButton::Right => RuffleMouseButton::Right, + }, + }, + AutomatedEvent::Wait => unreachable!(), + }); + }); + // Rendering has side-effects (such as processing 'DisplayObject.scrollRect' updates) + player.lock().unwrap().render(); + } + + // Render the image to disk + // FIXME: Determine how we want to compare against on on-disk image + #[cfg(feature = "imgtests")] + if check_img { + let mut player_lock = player.lock().unwrap(); + player_lock.render(); + let renderer = player_lock + .renderer_mut() + .downcast_mut::>() + .unwrap(); + + // Use straight alpha, since we want to save this as a PNG + let actual_image = renderer + .capture_frame(false) + .expect("Failed to capture image"); + + let info = renderer.descriptors().adapter.get_info(); + let suffix = format!("{}-{:?}", std::env::consts::OS, info.backend); + + let expected_image_path = base_path.join(format!("expected-{}.png", &suffix)); + let expected_image = image::open(&expected_image_path); + + let matches = match expected_image { + Ok(img) => { + img.as_rgba8().expect("Expected 8-bit RGBA image").as_raw() == actual_image.as_raw() + } + Err(e) => { + eprintln!( + "Failed to open expected image {:?}: {e:?}", + &expected_image_path + ); + false + } + }; + + if !matches { + let actual_image_path = base_path.join(format!("actual-{suffix}.png")); + actual_image.save_with_format(&actual_image_path, image::ImageFormat::Png)?; + panic!("Test output does not match expected image - saved actual image to {actual_image_path:?}"); + } + } + + before_end(player)?; + + executor.run(); + + let trace = trace_output.borrow().join("\n"); + Ok(trace) +}