tests: Move run_swf to util/runner

This commit is contained in:
Nathan Adams 2023-01-27 18:49:45 +01:00 committed by kmeisthax
parent 4418431bfe
commit 818897b74e
3 changed files with 204 additions and 191 deletions

View File

@ -5,20 +5,17 @@
use regex::Regex; use regex::Regex;
use ruffle_core::backend::{ use ruffle_core::backend::{
log::LogBackend, log::LogBackend,
navigator::{NullExecutor, NullNavigatorBackend},
storage::{MemoryStorageBackend, StorageBackend}, storage::{MemoryStorageBackend, StorageBackend},
}; };
use ruffle_core::context::UpdateContext; use ruffle_core::context::UpdateContext;
use ruffle_core::events::MouseButton as RuffleMouseButton;
use ruffle_core::external::Value as ExternalValue; use ruffle_core::external::Value as ExternalValue;
use ruffle_core::external::{ExternalInterfaceMethod, ExternalInterfaceProvider}; use ruffle_core::external::{ExternalInterfaceMethod, ExternalInterfaceProvider};
use ruffle_core::limits::ExecutionLimit; use ruffle_core::Player;
use ruffle_core::tag_utils::SwfMovie;
use ruffle_core::{Player, PlayerBuilder, PlayerEvent};
use ruffle_input_format::{AutomatedEvent, InputInjector, MouseButton as InputMouseButton};
use anyhow::Context; use anyhow::Context;
use anyhow::Result;
use libtest_mimic::{Arguments, Trial}; use libtest_mimic::{Arguments, Trial};
use ruffle_input_format::InputInjector;
#[cfg(feature = "imgtests")] #[cfg(feature = "imgtests")]
use ruffle_render_wgpu::backend::WgpuRenderBackend; use ruffle_render_wgpu::backend::WgpuRenderBackend;
#[cfg(feature = "imgtests")] #[cfg(feature = "imgtests")]
@ -28,7 +25,7 @@ use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Duration; use util::runner::run_swf;
use util::test::Test; use util::test::Test;
mod util; mod util;
@ -42,9 +39,7 @@ fn set_logger() {
.try_init(); .try_init();
} }
type Error = Box<dyn std::error::Error>; fn external_interface_avm1() -> Result<()> {
fn external_interface_avm1() -> Result<(), Error> {
set_logger(); set_logger();
test_swf_with_hooks( test_swf_with_hooks(
Path::new("tests/swfs/avm1/external_interface/test.swf"), 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(); set_logger();
test_swf_with_hooks( test_swf_with_hooks(
Path::new("tests/swfs/avm2/external_interface/test.swf"), 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(); set_logger();
// Test SharedObject persistence. Run an SWF that saves data // Test SharedObject persistence. Run an SWF that saves data
// to a shared object twice and verify that the data is saved. // to a shared object twice and verify that the data is saved.
@ -196,7 +191,7 @@ fn shared_object_avm1() -> Result<(), Error> {
Ok(()) Ok(())
} }
fn shared_object_avm2() -> Result<(), Error> { fn shared_object_avm2() -> Result<()> {
set_logger(); set_logger();
// Test SharedObject persistence. Run an SWF that saves data // Test SharedObject persistence. Run an SWF that saves data
// to a shared object twice and verify that the data is saved. // to a shared object twice and verify that the data is saved.
@ -257,11 +252,11 @@ fn test_swf_with_hooks(
num_frames: u32, num_frames: u32,
simulated_input_path: &Path, simulated_input_path: &Path,
expected_output_path: &Path, expected_output_path: &Path,
before_start: impl FnOnce(Arc<Mutex<Player>>) -> Result<(), Error>, before_start: impl FnOnce(Arc<Mutex<Player>>) -> Result<()>,
before_end: impl FnOnce(Arc<Mutex<Player>>) -> Result<(), Error>, before_end: impl FnOnce(Arc<Mutex<Player>>) -> Result<()>,
check_img: bool, check_img: bool,
frame_time_sleep: bool, frame_time_sleep: bool,
) -> Result<(), Error> { ) -> Result<()> {
let injector = let injector =
InputInjector::from_file(simulated_input_path).unwrap_or_else(|_| InputInjector::empty()); 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"); 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], num_patterns: &[Regex],
check_img: bool, check_img: bool,
approx_assert_fn: impl Fn(f64, f64), approx_assert_fn: impl Fn(f64, f64),
) -> Result<(), Error> { ) -> Result<()> {
let injector = let injector =
InputInjector::from_file(simulated_input_path).unwrap_or_else(|_| InputInjector::empty()); InputInjector::from_file(simulated_input_path).unwrap_or_else(|_| InputInjector::empty());
let trace_log = run_swf( let trace_log = run_swf(
@ -391,180 +386,6 @@ fn test_swf_approx(
Ok(()) 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<Mutex<Player>>) -> Result<(), Error>,
mut injector: InputInjector,
before_end: impl FnOnce(Arc<Mutex<Player>>) -> Result<(), Error>,
#[allow(unused)] mut check_img: bool,
frame_time_sleep: bool,
) -> Result<String, Error> {
#[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::<TextureTarget>::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::<WgpuRenderBackend<TextureTarget>>()
.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 { struct TestLogBackend {
trace_output: Rc<RefCell<Vec<String>>>, trace_output: Rc<RefCell<Vec<String>>>,
} }

View File

@ -2,6 +2,7 @@
// https://doc.rust-lang.org/book/ch11-03-test-organization.html // https://doc.rust-lang.org/book/ch11-03-test-organization.html
pub mod options; pub mod options;
pub mod runner;
pub mod test; pub mod test;
/// Wrapper around string slice that makes debug output `{:?}` to print string same way as `{}`. /// Wrapper around string slice that makes debug output `{:?}` to print string same way as `{}`.

191
tests/tests/util/runner.rs Normal file
View File

@ -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<Mutex<Player>>) -> Result<()>,
mut injector: InputInjector,
before_end: impl FnOnce(Arc<Mutex<Player>>) -> Result<()>,
#[allow(unused)] mut check_img: bool,
frame_time_sleep: bool,
) -> Result<String> {
#[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::<TextureTarget>::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::<WgpuRenderBackend<TextureTarget>>()
.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)
}