ruffle/core/tests/regression_tests.rs

420 lines
18 KiB
Rust
Raw Normal View History

//! Tests running SWFs in a headless Ruffle instance.
//!
//! Trace output can be compared with correct output from the official Flash Payer.
use approx::assert_abs_diff_eq;
use log::{Metadata, Record};
use ruffle_core::backend::navigator::{NullExecutor, NullNavigatorBackend};
2020-06-15 16:53:19 +00:00
use ruffle_core::backend::storage::MemoryStorageBackend;
2019-09-17 03:37:11 +00:00
use ruffle_core::backend::{
audio::NullAudioBackend, input::NullInputBackend, render::NullRenderer,
2019-09-17 03:37:11 +00:00
};
use ruffle_core::tag_utils::SwfMovie;
use ruffle_core::Player;
use std::cell::RefCell;
use std::path::Path;
use std::sync::Arc;
type Error = Box<dyn std::error::Error>;
// This macro generates test cases for a given list of SWFs.
macro_rules! swf_tests {
($($(#[$attr:meta])* ($name:ident, $path:expr, $num_frames:literal),)*) => {
$(
#[test]
$(#[$attr])*
fn $name() -> Result<(), Error> {
test_swf(
concat!("tests/swfs/", $path, "/test.swf"),
$num_frames,
concat!("tests/swfs/", $path, "/output.txt"),
)
}
)*
};
}
// This macro generates test cases for a given list of SWFs using `test_swf_approx`.
macro_rules! swf_tests_approx {
($($(#[$attr:meta])* ($name:ident, $path:expr, $num_frames:literal, $epsilon:literal),)*) => {
$(
#[test]
$(#[$attr])*
fn $name() -> Result<(), Error> {
test_swf_approx(
concat!("tests/swfs/", $path, "/test.swf"),
$num_frames,
concat!("tests/swfs/", $path, "/output.txt"),
$epsilon
)
}
)*
};
}
// List of SWFs to test.
// Format: (test_name, test_folder, number_of_frames_to_run)
// The test folder is a relative to core/tests/swfs
// Inside the folder is expected to be "test.swf" and "output.txt" with the correct output.
swf_tests! {
2019-12-03 23:01:39 +00:00
(add_property, "avm1/add_property", 1),
(as_transformed_flag, "avm1/as_transformed_flag", 3),
2020-07-28 11:17:27 +00:00
(as_broadcaster, "avm1/as_broadcaster", 1),
(as_broadcaster_initialize, "avm1/as_broadcaster_initialize", 1),
(attach_movie, "avm1/attach_movie", 1),
(function_base_clip, "avm1/function_base_clip", 2),
2020-01-14 07:38:20 +00:00
(call, "avm1/call", 2),
2020-01-04 03:48:39 +00:00
(color, "avm1/color", 1),
2019-12-01 20:16:02 +00:00
(clip_events, "avm1/clip_events", 4),
(create_empty_movie_clip, "avm1/create_empty_movie_clip", 2),
(empty_movieclip_can_attach_movies, "avm1/empty_movieclip_can_attach_movies", 1),
(duplicate_movie_clip, "avm1/duplicate_movie_clip", 1),
(mouse_listeners, "avm1/mouse_listeners", 1),
2019-10-29 17:49:44 +00:00
(do_init_action, "avm1/do_init_action", 3),
(execution_order1, "avm1/execution_order1", 3),
(execution_order2, "avm1/execution_order2", 15),
(execution_order3, "avm1/execution_order3", 5),
(single_frame, "avm1/single_frame", 2),
(looping, "avm1/looping", 6),
(matrix, "avm1/matrix", 1),
(point, "avm1/point", 1),
(rectangle, "avm1/rectangle", 1),
(goto_advance1, "avm1/goto_advance1", 2),
(goto_advance2, "avm1/goto_advance2", 2),
(goto_both_ways1, "avm1/goto_both_ways1", 2),
(goto_both_ways2, "avm1/goto_both_ways2", 3),
(goto_frame, "avm1/goto_frame", 3),
(goto_frame2, "avm1/goto_frame2", 5),
(goto_frame_number, "avm1/goto_frame_number", 4),
(goto_label, "avm1/goto_label", 4),
(goto_methods, "avm1/goto_methods", 1),
(goto_rewind1, "avm1/goto_rewind1", 4),
(goto_rewind2, "avm1/goto_rewind2", 5),
(goto_rewind3, "avm1/goto_rewind3", 2),
(goto_execution_order, "avm1/goto_execution_order", 3),
(goto_execution_order2, "avm1/goto_execution_order2", 2),
(greaterthan_swf5, "avm1/greaterthan_swf5", 1),
(greaterthan_swf8, "avm1/greaterthan_swf8", 1),
(strictly_equals, "avm1/strictly_equals", 1),
2019-09-17 17:36:25 +00:00
(tell_target, "avm1/tell_target", 3),
(typeofs, "avm1/typeof", 1),
(typeof_globals, "avm1/typeof_globals", 1),
(closure_scope, "avm1/closure_scope", 1),
(variable_args, "avm1/variable_args", 1),
(custom_clip_methods, "avm1/custom_clip_methods", 3),
(delete, "avm1/delete", 3),
(default_names, "avm1/default_names", 6),
2019-11-27 22:54:19 +00:00
(array_trivial, "avm1/array_trivial", 1),
(array_concat, "avm1/array_concat", 1),
(array_slice, "avm1/array_slice", 1),
(array_splice, "avm1/array_splice", 1),
(array_properties, "avm1/array_properties", 1),
(array_prototyping, "avm1/array_prototyping", 1),
(array_vs_object_length, "avm1/array_vs_object_length", 1),
2020-04-01 11:24:46 +00:00
(array_sort, "avm1/array_sort", 1),
(array_enumerate, "avm1/array_enumerate", 1),
(timeline_function_def, "avm1/timeline_function_def", 3),
(root_global_parent, "avm1/root_global_parent", 3),
(register_underflow, "avm1/register_underflow", 1),
2019-11-26 22:06:26 +00:00
(object_prototypes, "avm1/object_prototypes", 1),
(movieclip_prototype_extension, "avm1/movieclip_prototype_extension", 1),
(movieclip_hittest, "avm1/movieclip_hittest", 1),
#[ignore] (textfield_text, "avm1/textfield_text", 1),
(recursive_prototypes, "avm1/recursive_prototypes", 2),
2019-12-03 08:53:43 +00:00
(stage_object_children, "avm1/stage_object_children", 2),
2019-11-27 19:51:40 +00:00
(has_own_property, "avm1/has_own_property", 1),
(extends_chain, "avm1/extends_chain", 1),
2019-11-27 20:08:41 +00:00
(is_prototype_of, "avm1/is_prototype_of", 1),
#[ignore] (string_coercion, "avm1/string_coercion", 1),
(lessthan_swf4, "avm1/lessthan_swf4", 1),
(lessthan2_swf5, "avm1/lessthan2_swf5", 1),
(lessthan2_swf6, "avm1/lessthan2_swf6", 1),
(lessthan2_swf7, "avm1/lessthan2_swf7", 1),
2019-12-22 06:40:32 +00:00
(logical_ops_swf4, "avm1/logical_ops_swf4", 1),
(logical_ops_swf8, "avm1/logical_ops_swf8", 1),
(movieclip_depth_methods, "avm1/movieclip_depth_methods", 3),
(get_variable_in_scope, "avm1/get_variable_in_scope", 1),
(movieclip_init_object, "avm1/movieclip_init_object", 1),
(greater_swf6, "avm1/greater_swf6", 1),
(greater_swf7, "avm1/greater_swf7", 1),
(equals_swf4, "avm1/equals_swf4", 1),
(equals2_swf5, "avm1/equals2_swf5", 1),
(equals2_swf6, "avm1/equals2_swf6", 1),
(equals2_swf7, "avm1/equals2_swf7", 1),
2020-01-20 21:46:46 +00:00
(register_class, "avm1/register_class", 1),
(register_and_init_order, "avm1/register_and_init_order", 1),
(on_construct, "avm1/on_construct", 1),
(set_variable_scope, "avm1/set_variable_scope", 1),
2019-12-16 23:33:57 +00:00
(slash_syntax, "avm1/slash_syntax", 2),
(strictequals_swf6, "avm1/strictequals_swf6", 1),
2020-01-18 22:48:05 +00:00
(string_methods, "avm1/string_methods", 1),
2020-01-18 02:05:26 +00:00
(target_path, "avm1/target_path", 1),
(global_is_bare, "avm1/global_is_bare", 1),
2020-01-20 08:20:27 +00:00
(primitive_type_globals, "avm1/primitive_type_globals", 1),
(primitive_instanceof, "avm1/primitive_instanceof", 1),
(as2_oop, "avm1/as2_oop", 1),
2019-12-22 05:21:41 +00:00
(xml, "avm1/xml", 1),
2019-12-25 22:42:38 +00:00
(xml_namespaces, "avm1/xml_namespaces", 1),
(xml_node_namespaceuri, "avm1/xml_node_namespaceuri", 1),
(xml_node_weirdnamespace, "avm1/xml_node_weirdnamespace", 1),
2019-12-25 23:01:59 +00:00
(xml_clone_expandos, "avm1/xml_clone_expandos", 1),
(xml_has_child_nodes, "avm1/xml_has_child_nodes", 1),
(xml_first_last_child, "avm1/xml_first_last_child", 1),
2019-12-26 01:42:54 +00:00
(xml_parent_and_child, "avm1/xml_parent_and_child", 1),
(xml_siblings, "avm1/xml_siblings", 1),
(xml_attributes_read, "avm1/xml_attributes_read", 1),
2019-12-26 06:46:25 +00:00
(xml_append_child, "avm1/xml_append_child", 1),
(xml_append_child_with_parent, "avm1/xml_append_child_with_parent", 1),
2019-12-28 03:56:03 +00:00
(xml_remove_node, "avm1/xml_remove_node", 1),
2019-12-28 20:32:13 +00:00
(xml_insert_before, "avm1/xml_insert_before", 1),
2019-12-28 22:15:27 +00:00
(xml_to_string, "avm1/xml_to_string", 1),
(xml_to_string_comment, "avm1/xml_to_string_comment", 1),
2020-01-01 21:04:58 +00:00
(xml_idmap, "avm1/xml_idmap", 1),
2019-12-30 04:05:06 +00:00
(xml_inspect_doctype, "avm1/xml_inspect_doctype", 1),
#[ignore] (xml_inspect_xmldecl, "avm1/xml_inspect_xmldecl", 1),
(xml_inspect_createmethods, "avm1/xml_inspect_createmethods", 1),
2020-01-01 00:48:36 +00:00
(xml_inspect_parsexml, "avm1/xml_inspect_parsexml", 1),
(funky_function_calls, "avm1/funky_function_calls", 1),
(undefined_to_string_swf6, "avm1/undefined_to_string_swf6", 1),
(define_function2_preload, "avm1/define_function2_preload", 1),
(define_function2_preload_order, "avm1/define_function2_preload_order", 1),
(mcl_as_broadcaster, "avm1/mcl_as_broadcaster", 1),
2020-06-20 23:02:45 +00:00
(uncaught_exception, "avm1/uncaught_exception", 1),
(uncaught_exception_bubbled, "avm1/uncaught_exception_bubbled", 1),
(try_catch_finally, "avm1/try_catch_finally", 1),
(try_finally_simple, "avm1/try_finally_simple", 1),
(loadmovie, "avm1/loadmovie", 2),
2020-01-17 00:33:09 +00:00
(loadmovienum, "avm1/loadmovienum", 2),
(loadmovie_registerclass, "avm1/loadmovie_registerclass", 2),
2020-01-17 00:37:38 +00:00
(loadmovie_method, "avm1/loadmovie_method", 2),
(unloadmovie, "avm1/unloadmovie", 11),
Rewrite some of the unload and cliploader tests to be more generous with load timing. Loads in Flash Player, like all web technologies, are asynchronous tasks of some kind (probably a separate thread). They appear to operate on some kind of a delay. If I `trace` each frame out, like in the previous version of `mcl_loadclip`, you get a series of events that look like this: 1. Parent frame 1 2. Parent frame 2 3. Event: onLoadStart 4. Event: onLoadProgress 5. Event: onLoadComplete 6. Parent frame 3 7. Event: onLoadInit If I run that version of the test on Ruffle, everything happens after frame 1. This is an artifact of how we're testing asynchronous behavior in Ruffle. In order to guarantee test determinism, we have a dummy implementation of `fetch` that does a blocking load, and we poll all futures every frame of execution. This means that there is a very specific order of execution with these tests, which is good for testing, but probably isn't 100% accurate. Flash Player appears to delay all loads by at least one frame, even loads that are coming from disk which should load immediately. I don't know if this is intentional or not, so I don't want to implement a load delay just for the sake of making tests pass. Ergo, I'm loosening the tests to just test the ability to load and unload movies, and fire events from a loader. Specifically: 1. `mcl_loadclip` no longer traces out frames of the parent timeline 2. `unloadmovie` et. all use a target movie that doesn't fail the test until 10 frames have passed. If someone can find a movie network that breaks with fast loading, then I'll consider implementing explicit frame delays for async tasks. Otherwise, this is how we're testing this.
2020-01-18 00:00:41 +00:00
(unloadmovienum, "avm1/unloadmovienum", 11),
(unloadmovie_method, "avm1/unloadmovie_method", 11),
(mcl_loadclip, "avm1/mcl_loadclip", 11),
(mcl_unloadclip, "avm1/mcl_unloadclip", 11),
(mcl_getprogress, "avm1/mcl_getprogress", 6),
2020-07-23 01:22:52 +00:00
(load_vars, "avm1/load_vars", 2),
(loadvariables, "avm1/loadvariables", 3),
(loadvariablesnum, "avm1/loadvariablesnum", 3),
(loadvariables_method, "avm1/loadvariables_method", 3),
(xml_load, "avm1/xml_load", 1),
(with_return, "avm1/with_return", 1),
(watch, "avm1/watch", 1),
#[ignore] (watch_virtual_property, "avm1/watch_virtual_property", 1),
(cross_movie_root, "avm1/cross_movie_root", 5),
(roots_and_levels, "avm1/roots_and_levels", 1),
(swf6_case_insensitive, "avm1/swf6_case_insensitive", 1),
(swf7_case_sensitive, "avm1/swf7_case_sensitive", 1),
(prototype_enumerate, "avm1/prototype_enumerate", 1),
(stage_object_enumerate, "avm1/stage_object_enumerate", 1),
(new_object_enumerate, "avm1/new_object_enumerate", 1),
(as2_super_and_this_v6, "avm1/as2_super_and_this_v6", 1),
(as2_super_and_this_v8, "avm1/as2_super_and_this_v8", 1),
(as2_super_via_manual_prototype, "avm1/as2_super_via_manual_prototype", 1),
(as1_constructor_v6, "avm1/as1_constructor_v6", 1),
(as1_constructor_v7, "avm1/as1_constructor_v7", 1),
2020-06-13 09:20:46 +00:00
(issue_710, "avm1/issue_710", 1),
(infinite_recursion_function, "avm1/infinite_recursion_function", 1),
(infinite_recursion_function_in_setter, "avm1/infinite_recursion_function_in_setter", 1),
(infinite_recursion_virtual_property, "avm1/infinite_recursion_virtual_property", 1),
(edittext_font_size, "avm1/edittext_font_size", 1),
(edittext_default_format, "avm1/edittext_default_format", 1),
(edittext_leading, "avm1/edittext_leading", 1),
#[ignore] (edittext_newlines, "avm1/edittext_newlines", 1),
(edittext_html_entity, "avm1/edittext_html_entity", 1),
#[ignore] (edittext_html_roundtrip, "avm1/edittext_html_roundtrip", 1),
(define_local, "avm1/define_local", 1),
2020-06-23 06:10:53 +00:00
(textfield_variable, "avm1/textfield_variable", 8),
2020-07-03 03:18:30 +00:00
(error, "avm1/error", 1),
(color_transform, "avm1/color_transform", 1),
(with, "avm1/with", 1),
(arguments, "avm1/arguments", 1),
(prototype_properties, "avm1/prototype_properties", 1),
(stage_object_properties_get_var, "avm1/stage_object_properties_get_var", 1),
2020-07-08 02:06:03 +00:00
(set_interval, "avm1/set_interval", 20),
(context_menu, "avm1/context_menu", 1),
(context_menu_item, "avm1/context_menu_item", 1),
(constructor_function, "avm1/constructor_function", 1),
2020-07-24 18:06:48 +00:00
(global_array, "avm1/global_array", 1),
(array_constructor, "avm1/array_constructor", 1),
(array_apply, "avm1/array_constructor", 1),
2020-07-27 16:20:49 +00:00
(object_function, "avm1/object_function", 1),
(as3_hello_world, "avm2/hello_world", 1),
2020-02-24 19:12:51 +00:00
(as3_function_call, "avm2/function_call", 1),
2020-03-09 02:29:15 +00:00
(as3_function_call_via_call, "avm2/function_call_via_call", 1),
(as3_constructor_call, "avm2/constructor_call", 1),
(as3_class_methods, "avm2/class_methods", 1),
(as3_es3_inheritance, "avm2/es3_inheritance", 1),
(as3_es4_inheritance, "avm2/es4_inheritance", 1),
(as3_stored_properties, "avm2/stored_properties", 1),
(as3_virtual_properties, "avm2/virtual_properties", 1),
(as3_es4_oop_prototypes, "avm2/es4_oop_prototypes", 1),
(as3_es4_method_binding, "avm2/es4_method_binding", 1),
(as3_control_flow_bool, "avm2/control_flow_bool", 1),
(as3_control_flow_stricteq, "avm2/control_flow_stricteq", 1),
(as3_object_enumeration, "avm2/object_enumeration", 1),
(as3_class_enumeration, "avm2/class_enumeration", 1),
(as3_is_prototype_of, "avm2/is_prototype_of", 1),
(as3_has_own_property, "avm2/has_own_property", 1),
2020-03-08 01:17:07 +00:00
(as3_property_is_enumerable, "avm2/property_is_enumerable", 1),
2020-03-08 23:12:40 +00:00
(as3_set_property_is_enumerable, "avm2/set_property_is_enumerable", 1),
(as3_object_to_string, "avm2/object_to_string", 1),
(as3_function_to_string, "avm2/function_to_string", 1),
(as3_class_to_string, "avm2/class_to_string", 1),
(as3_object_to_locale_string, "avm2/object_to_locale_string", 1),
(as3_function_to_locale_string, "avm2/function_to_locale_string", 1),
(as3_class_to_locale_string, "avm2/class_to_locale_string", 1),
2020-03-09 02:49:52 +00:00
(as3_object_value_of, "avm2/object_value_of", 1),
(as3_function_value_of, "avm2/function_value_of", 1),
(as3_class_value_of, "avm2/class_value_of", 1),
(as3_if_stricteq, "avm2/if_stricteq", 1),
(as3_if_strictne, "avm2/if_strictne", 1),
(as3_strict_equality, "avm2/strict_equality", 1),
(as3_es4_interfaces, "avm2/es4_interfaces", 1),
(as3_istype, "avm2/istype", 1),
(as3_instanceof, "avm2/instanceof", 1),
}
// TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough.
// Eventually we can hopefully make some of these match exactly (see #193).
// Some will probably always need to be approx. (if they rely on trig functions, etc.)
swf_tests_approx! {
2020-02-18 19:39:53 +00:00
(local_to_global, "avm1/local_to_global", 1, 0.051),
(stage_object_properties, "avm1/stage_object_properties", 4, 0.051),
(stage_object_properties_swf6, "avm1/stage_object_properties_swf6", 4, 0.051),
2020-02-18 19:39:53 +00:00
(movieclip_getbounds, "avm1/movieclip_getbounds", 1, 0.051),
(edittext_letter_spacing, "avm1/edittext_letter_spacing", 1, 15.0), // TODO: Discrepancy in wrapping in letterSpacing = 0.1 test.
(edittext_align, "avm1/edittext_align", 1, 3.0),
(edittext_margins, "avm1/edittext_margins", 1, 5.0), // TODO: Discrepancy in wrapping.
(edittext_tab_stops, "avm1/edittext_tab_stops", 1, 5.0),
(edittext_bullet, "avm1/edittext_bullet", 1, 3.0),
(edittext_underline, "avm1/edittext_underline", 1, 4.0),
}
/// Wrapper around string slice that makes debug output `{:?}` to print string same way as `{}`.
/// Used in different `assert*!` macros in combination with `pretty_assertions` crate to make
/// test failures to show nice diffs.
/// Courtesy of https://github.com/colin-kiegel/rust-pretty-assertions/issues/24
#[derive(PartialEq, Eq)]
#[doc(hidden)]
pub struct PrettyString<'a>(pub &'a str);
/// Make diff to display string as multi-line string
impl<'a> std::fmt::Debug for PrettyString<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str(self.0)
}
}
macro_rules! assert_eq {
($left:expr, $right:expr) => {
pretty_assertions::assert_eq!(PrettyString($left.as_ref()), PrettyString($right.as_ref()));
};
($left:expr, $right:expr, $message:expr) => {
pretty_assertions::assert_eq!(
PrettyString($left.as_ref()),
PrettyString($right.as_ref()),
$message
);
};
}
/// 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 test_swf(swf_path: &str, num_frames: u32, expected_output_path: &str) -> Result<(), Error> {
let expected_output = std::fs::read_to_string(expected_output_path)?.replace("\r\n", "\n");
let trace_log = run_swf(swf_path, num_frames)?;
assert_eq!(
trace_log, expected_output,
"ruffle output != flash player output"
);
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.
/// If a line has a floating point value, it will be compared approxinmately using the given epsilon.
fn test_swf_approx(
swf_path: &str,
num_frames: u32,
expected_output_path: &str,
epsilon: f64,
) -> Result<(), Error> {
let trace_log = run_swf(swf_path, num_frames)?;
let expected_data = std::fs::read_to_string(expected_output_path)?;
std::assert_eq!(
trace_log.lines().count(),
expected_data.lines().count(),
"# of lines of output didn't match"
);
for (actual, expected) in trace_log.lines().zip(expected_data.lines()) {
// If these are numbers, compare using approx_eq.
if let (Ok(actual), Ok(expected)) = (actual.parse::<f64>(), expected.parse::<f64>()) {
// TODO: Lower this epsilon as the accuracy of the properties improves.
assert_abs_diff_eq!(actual, expected, epsilon = epsilon);
} else {
assert_eq!(actual, expected);
}
}
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: &str, num_frames: u32) -> Result<String, Error> {
let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info));
let base_path = Path::new(swf_path).parent().unwrap();
let (mut executor, channel) = NullExecutor::new();
let movie = SwfMovie::from_path(swf_path)?;
2020-07-08 02:06:03 +00:00
let frame_time = 1000.0 / movie.header().frame_rate as f64;
let player = Player::new(
Box::new(NullRenderer),
Box::new(NullAudioBackend::new()),
Box::new(NullNavigatorBackend::with_base_path(base_path, channel)),
Box::new(NullInputBackend::new()),
2020-06-15 16:53:19 +00:00
Box::new(MemoryStorageBackend::default()),
2019-09-17 03:37:11 +00:00
)?;
player.lock().unwrap().set_root_movie(Arc::new(movie));
for _ in 0..num_frames {
player.lock().unwrap().run_frame();
2020-07-08 02:06:03 +00:00
player.lock().unwrap().update_timers(frame_time);
executor.poll_all().unwrap();
}
executor.block_all().unwrap();
Ok(trace_log())
}
thread_local! {
static TRACE_LOG: RefCell<String> = RefCell::new(String::new());
}
static TRACE_LOGGER: TraceLogger = TraceLogger;
/// `TraceLogger` captures output from AVM trace actions into a String.
struct TraceLogger;
fn trace_log() -> String {
TRACE_LOG.with(|log| log.borrow().clone())
}
impl log::Log for TraceLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.target() == "avm_trace"
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
TRACE_LOG.with(|log| log.borrow_mut().push_str(&format!("{}\n", record.args())));
}
}
fn flush(&self) {}
}