diff --git a/.github/workflows/snapshot_benchmark_main.yml b/.github/workflows/snapshot_benchmark_main.yml new file mode 100644 index 0000000000..283ffbdb2c --- /dev/null +++ b/.github/workflows/snapshot_benchmark_main.yml @@ -0,0 +1,32 @@ +on: + push: + branches: main + +jobs: + benchmark_base_branch: + name: Continuous Benchmarking with Bencher + permissions: + checks: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run Xtask initializer + run: | + cargo xtask init + - uses: bencherdev/bencher@main + - name: Track base branch benchmarks with Bencher + run: | + bencher run \ + --project bms \ + --token '${{ secrets.BENCHER_API_TOKEN }}' \ + --branch main \ + --testbed ubuntu-latest \ + --threshold-measure latency \ + --threshold-test t_test \ + --threshold-max-sample-size 64 \ + --threshold-upper-boundary 0.99 \ + --thresholds-reset \ + --err \ + --adapter json \ + --github-actions '${{ secrets.GITHUB_TOKEN }}' \ + bencher run --adapter rust_criterion "cargo bench --features lua54" \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 7c14693ebe..3044400cce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,10 @@ features = ["lua54", "rhai"] [features] default = ["core_functions", "bevy_bindings"] -## lua -lua = ["bevy_mod_scripting_lua", "bevy_mod_scripting_functions/lua_bindings"] +lua = [ + "bevy_mod_scripting_lua", + "bevy_mod_scripting_functions/lua_bindings", +] ## lua # one of these must be selected lua51 = ["bevy_mod_scripting_lua/lua51", "lua"] lua52 = ["bevy_mod_scripting_lua/lua52", "lua"] @@ -76,8 +78,12 @@ clap = { version = "4.1", features = ["derive"] } rand = "0.8.5" bevy_console = "0.13" # rhai-rand = "0.1" +criterion = { version = "0.5" } ansi-parser = "0.9" ladfile_builder = { path = "crates/ladfile_builder", version = "0.2.6" } +script_integration_test_harness = { workspace = true } +test_utils = { workspace = true } +libtest-mimic = "0.8" [workspace] members = [ @@ -149,3 +155,11 @@ todo = "deny" [workspace.lints.rust] missing_docs = "deny" + +[[bench]] +name = "benchmarks" +harness = false + +[[test]] +name = "script_tests" +harness = false diff --git a/assets/benchmarks/component/access.lua b/assets/benchmarks/component/access.lua new file mode 100644 index 0000000000..639a25ed0f --- /dev/null +++ b/assets/benchmarks/component/access.lua @@ -0,0 +1,5 @@ +local entity_with_component = world._get_entity_with_test_component("TestComponent") + +function bench() + local strings = world.get_component(entity_with_component, types.TestComponent).strings +end \ No newline at end of file diff --git a/assets/benchmarks/component/access.rhai b/assets/benchmarks/component/access.rhai new file mode 100644 index 0000000000..1e5b7db5e6 --- /dev/null +++ b/assets/benchmarks/component/access.rhai @@ -0,0 +1,5 @@ +let entity_with_component = world._get_entity_with_test_component.call("TestComponent"); + +fn bench(){ + let strings = world.get_component.call(entity_with_component, types.TestComponent).strings; +} \ No newline at end of file diff --git a/assets/benchmarks/component/get.lua b/assets/benchmarks/component/get.lua new file mode 100644 index 0000000000..6a733e717e --- /dev/null +++ b/assets/benchmarks/component/get.lua @@ -0,0 +1,5 @@ +local entity_with_component = world._get_entity_with_test_component("TestComponent") + +function bench() + world.get_component(entity_with_component, types.TestComponent) +end \ No newline at end of file diff --git a/assets/benchmarks/component/get.rhai b/assets/benchmarks/component/get.rhai new file mode 100644 index 0000000000..75cf5e7cc2 --- /dev/null +++ b/assets/benchmarks/component/get.rhai @@ -0,0 +1,5 @@ +let entity_with_component = world._get_entity_with_test_component.call("TestComponent"); + +fn bench(){ + world.get_component.call(entity_with_component, types.TestComponent); +} \ No newline at end of file diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs new file mode 100644 index 0000000000..76766cfe47 --- /dev/null +++ b/benches/benchmarks.rs @@ -0,0 +1,95 @@ +use std::path::PathBuf; + +use bevy::utils::HashMap; +use criterion::{ + criterion_group, criterion_main, measurement::Measurement, BenchmarkGroup, Criterion, +}; +use script_integration_test_harness::{run_lua_benchmark, run_rhai_benchmark}; +use test_utils::{discover_all_tests, Test}; + +extern crate bevy_mod_scripting; +extern crate script_integration_test_harness; +extern crate test_utils; + +pub trait BenchmarkExecutor { + fn benchmark_group(&self) -> String; + fn benchmark_name(&self) -> String; + fn execute(&self, criterion: &mut BenchmarkGroup); +} + +impl BenchmarkExecutor for Test { + fn benchmark_group(&self) -> String { + // we want to use OS agnostic paths + // use the file path from `benchmarks` onwards using folders as groupings + // replace file separators with `/` + // replace _ with spaces + let path = self.path.to_string_lossy(); + let path = path.split("benchmarks").collect::>()[1] + .replace(std::path::MAIN_SEPARATOR, "/"); + let first_folder = path.split("/").collect::>()[1]; + first_folder.replace("_", " ") + } + + fn benchmark_name(&self) -> String { + // use just the file stem + let name = self + .path + .file_stem() + .unwrap() + .to_string_lossy() + .to_string() + .replace("_", " "); + + let language = self.kind.to_string(); + + format!("{name} {language}") + } + + fn execute(&self, criterion: &mut BenchmarkGroup) { + match self.kind { + test_utils::TestKind::Lua => run_lua_benchmark( + &self.path.to_string_lossy(), + &self.benchmark_name(), + criterion, + ) + .expect("Benchmark failed"), + test_utils::TestKind::Rhai => run_rhai_benchmark( + &self.path.to_string_lossy(), + &self.benchmark_name(), + criterion, + ) + .expect("benchmark failed"), + } + } +} + +fn script_benchmarks(criterion: &mut Criterion) { + // find manifest dir + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let tests = discover_all_tests(manifest_dir, |p| p.starts_with("benchmarks")); + + // group by benchmark group + let mut grouped: HashMap> = + tests.into_iter().fold(HashMap::default(), |mut acc, t| { + acc.entry(t.benchmark_group()).or_default().push(t); + acc + }); + + // sort within groups by benchmark name + for (_, tests) in grouped.iter_mut() { + tests.sort_by_key(|a| a.benchmark_name()); + } + + for (group, tests) in grouped { + let mut benchmark_group = criterion.benchmark_group(group); + + for t in tests { + t.execute(&mut benchmark_group); + } + + benchmark_group.finish(); + } +} + +criterion_group!(benches, script_benchmarks); +criterion_main!(benches); diff --git a/crates/bevy_mod_scripting_core/src/script.rs b/crates/bevy_mod_scripting_core/src/script.rs index 64a93be8bd..c4fd82a5ea 100644 --- a/crates/bevy_mod_scripting_core/src/script.rs +++ b/crates/bevy_mod_scripting_core/src/script.rs @@ -39,6 +39,44 @@ pub struct Scripts { pub(crate) scripts: HashMap>, } +impl Scripts

{ + /// Inserts a script into the collection + pub fn insert(&mut self, script: Script

) { + self.scripts.insert(script.id.clone(), script); + } + + /// Removes a script from the collection, returning `true` if the script was in the collection, `false` otherwise + pub fn remove>(&mut self, script: S) -> bool { + self.scripts.remove(&script.into()).is_some() + } + + /// Checks if a script is in the collection + /// Returns `true` if the script is in the collection, `false` otherwise + pub fn contains>(&self, script: S) -> bool { + self.scripts.contains_key(&script.into()) + } + + /// Returns a reference to the script with the given id + pub fn get>(&self, script: S) -> Option<&Script

> { + self.scripts.get(&script.into()) + } + + /// Returns a mutable reference to the script with the given id + pub fn get_mut>(&mut self, script: S) -> Option<&mut Script

> { + self.scripts.get_mut(&script.into()) + } + + /// Returns an iterator over the scripts + pub fn iter(&self) -> impl Iterator> { + self.scripts.values() + } + + /// Returns a mutable iterator over the scripts + pub fn iter_mut(&mut self) -> impl Iterator> { + self.scripts.values_mut() + } +} + impl Default for Scripts

{ fn default() -> Self { Self { diff --git a/crates/languages/bevy_mod_scripting_lua/Cargo.toml b/crates/languages/bevy_mod_scripting_lua/Cargo.toml index 7193a502c7..3d8c55919d 100644 --- a/crates/languages/bevy_mod_scripting_lua/Cargo.toml +++ b/crates/languages/bevy_mod_scripting_lua/Cargo.toml @@ -46,14 +46,5 @@ smol_str = "0.2.2" smallvec = "1.13" profiling = { workspace = true } -[dev-dependencies] -script_integration_test_harness = { workspace = true } -libtest-mimic = "0.8" -regex = "1.11" - -[[test]] -name = "lua_tests" -harness = false - [lints] workspace = true diff --git a/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_unit_struct.lua b/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_unit_struct.lua deleted file mode 100644 index 4f574e75aa..0000000000 --- a/crates/languages/bevy_mod_scripting_lua/tests/data/construct/construct_unit_struct.lua +++ /dev/null @@ -1,4 +0,0 @@ -local type = world.get_type_by_name("UnitStruct") -local constructed = construct(type, {}) - -assert(constructed ~= nil, "Value was not constructed") diff --git a/crates/languages/bevy_mod_scripting_lua/tests/lua_tests.rs b/crates/languages/bevy_mod_scripting_lua/tests/lua_tests.rs deleted file mode 100644 index cacfd11bb6..0000000000 --- a/crates/languages/bevy_mod_scripting_lua/tests/lua_tests.rs +++ /dev/null @@ -1,139 +0,0 @@ -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, missing_docs)] -use bevy_mod_scripting_core::{ - bindings::{pretty_print::DisplayWithWorld, ThreadWorldContainer, WorldContainer}, - error::ScriptError, - ConfigureScriptPlugin, -}; -use bevy_mod_scripting_lua::LuaScriptingPlugin; -use libtest_mimic::{Arguments, Failed, Trial}; -use mlua::{Function, Lua, MultiValue}; -use script_integration_test_harness::execute_integration_test; -use std::{ - fs::{self, DirEntry}, - io, panic, - path::{Path, PathBuf}, -}; - -#[derive(Debug)] -struct Test { - path: PathBuf, -} - -impl Test { - fn execute(self) -> Result<(), Failed> { - println!("Running test: {:?}", self.path); - - execute_integration_test::( - |world, type_registry| { - let _ = world; - let _ = type_registry; - }, - |app| { - app.add_plugins(LuaScriptingPlugin::default().add_context_initializer(|_,ctxt: &mut Lua| { - let globals = ctxt.globals(); - globals.set( - "assert_throws", - ctxt.create_function(|_lua, (f, reg): (Function, String)| { - let world = ThreadWorldContainer.try_get_world()?; - let result = f.call::<()>(MultiValue::new()); - let err = match result { - Ok(_) => { - return Err(mlua::Error::external( - "Expected function to throw error, but it did not.", - )) - } - Err(e) => - ScriptError::from_mlua_error(e).display_with_world(world) - , - }; - - let regex = regex::Regex::new(®).unwrap(); - if regex.is_match(&err) { - Ok(()) - } else { - Err(mlua::Error::external( - format!( - "Expected error message to match the regex: \n{}\n\nBut got:\n{}", - regex.as_str(), - err - ), - )) - } - })?, - )?; - Ok(()) - })); - }, - self.path.as_os_str().to_str().unwrap(), - ) - .map_err(Failed::from) - } - - fn name(&self) -> String { - format!( - "script_test - lua - {}", - self.path - .to_string_lossy() - .split(&format!("tests{}data", std::path::MAIN_SEPARATOR)) - .last() - .unwrap() - ) - } -} - -fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&DirEntry)) -> io::Result<()> { - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - visit_dirs(&path, cb)?; - } else { - cb(&entry); - } - } - } else { - panic!("Not a directory: {:?}", dir); - } - Ok(()) -} - -fn discover_all_tests() -> Vec { - let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let assets_root = workspace_root - .join("..") - .join("..") - .join("..") - .join("assets"); - let test_root = assets_root.join("tests"); - let mut test_files = Vec::new(); - visit_dirs(&test_root, &mut |entry| { - let path = entry.path(); - if path.extension().unwrap() == "lua" { - // only take the path from the assets bit - let relative = path.strip_prefix(&assets_root).unwrap(); - test_files.push(Test { - path: relative.to_path_buf(), - }); - } - }) - .unwrap(); - - test_files -} - -// run this with `cargo test --features lua54 --package bevy_mod_scripting_lua --test lua_tests` -// or filter using the prefix "lua test -" -fn main() { - // Parse command line arguments - let args = Arguments::from_args(); - - // Create a list of tests and/or benchmarks (in this case: two dummy tests). - let all_tests = discover_all_tests(); - println!("discovered {} tests. {:?}", all_tests.len(), all_tests); - let tests = all_tests - .into_iter() - .map(|t| Trial::test(t.name(), move || t.execute())); - // Run all tests and exit the application appropriatly. - libtest_mimic::run(&args, tests.collect()).exit(); -} diff --git a/crates/languages/bevy_mod_scripting_rhai/Cargo.toml b/crates/languages/bevy_mod_scripting_rhai/Cargo.toml index fd9f10783a..a017fa2e4e 100644 --- a/crates/languages/bevy_mod_scripting_rhai/Cargo.toml +++ b/crates/languages/bevy_mod_scripting_rhai/Cargo.toml @@ -22,14 +22,5 @@ bevy_mod_scripting_core = { workspace = true, features = ["rhai_impls"] } strum = { version = "0.26", features = ["derive"] } parking_lot = "0.12.1" -[dev-dependencies] -script_integration_test_harness = { workspace = true } -libtest-mimic = "0.8" -regex = "1.11" - -[[test]] -name = "rhai_tests" -harness = false - [lints] workspace = true diff --git a/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/unit_struct.rhai b/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/unit_struct.rhai deleted file mode 100644 index 2b5840df44..0000000000 --- a/crates/languages/bevy_mod_scripting_rhai/tests/data/construct/unit_struct.rhai +++ /dev/null @@ -1,2 +0,0 @@ -let type = world.get_type_by_name.call("UnitStruct"); -let constructed = construct.call(type, #{}); diff --git a/crates/languages/bevy_mod_scripting_rhai/tests/rhai_tests.rs b/crates/languages/bevy_mod_scripting_rhai/tests/rhai_tests.rs deleted file mode 100644 index 479f030a4e..0000000000 --- a/crates/languages/bevy_mod_scripting_rhai/tests/rhai_tests.rs +++ /dev/null @@ -1,151 +0,0 @@ -#![allow( - clippy::unwrap_used, - clippy::todo, - clippy::expect_used, - clippy::panic, - missing_docs -)] -use bevy_mod_scripting_core::{ - bindings::{pretty_print::DisplayWithWorld, ThreadWorldContainer, WorldContainer}, - error::ScriptError, - AddRuntimeInitializer, -}; -use bevy_mod_scripting_rhai::RhaiScriptingPlugin; -use libtest_mimic::{Arguments, Failed, Trial}; -use rhai::{Dynamic, EvalAltResult, FnPtr, NativeCallContext}; -use script_integration_test_harness::execute_integration_test; -use std::{ - fs::{self, DirEntry}, - io, panic, - path::{Path, PathBuf}, -}; - -struct Test { - path: PathBuf, -} - -impl Test { - fn execute(self) -> Result<(), Failed> { - execute_integration_test::( - |world, type_registry| { - let _ = world; - let _ = type_registry; - }, - |app| { - app.add_plugins(RhaiScriptingPlugin::default()); - app.add_runtime_initializer::(|runtime| { - let mut runtime = runtime.write(); - - runtime.register_fn("assert", |a: Dynamic, b: &str| { - if !a.is::() { - panic!("Expected a boolean value, but got {:?}", a); - } - if !a.as_bool().unwrap() { - panic!("Assertion failed. {}", b); - } - }); - - runtime.register_fn("assert", |a: Dynamic| { - if !a.is::() { - panic!("Expected a boolean value, but got {:?}", a); - } - if !a.as_bool().unwrap() { - panic!("Assertion failed"); - } - }); - runtime.register_fn("assert_throws", |ctxt: NativeCallContext, fn_: FnPtr, regex: String| { - let world = ThreadWorldContainer.try_get_world()?; - let args: [Dynamic;0] = []; - let result = fn_.call_within_context::<()>(&ctxt, args); - match result { - Ok(_) => panic!("Expected function to throw error, but it did not."), - Err(e) => { - let e = ScriptError::from_rhai_error(*e); - let err = e.display_with_world(world); - let regex = regex::Regex::new(®ex).unwrap(); - if regex.is_match(&err) { - Ok::<(), Box>(()) - } else { - panic!( - "Expected error message to match the regex: \n{}\n\nBut got:\n{}", - regex.as_str(), - err - ) - } - }, - } - }); - Ok(()) - }); - }, - self.path.as_os_str().to_str().unwrap(), - ) - .map_err(Failed::from) - } - - fn name(&self) -> String { - format!( - "script_test - lua - {}", - self.path - .to_string_lossy() - .split(&format!("tests{}data", std::path::MAIN_SEPARATOR)) - .last() - .unwrap() - ) - } -} - -fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&DirEntry)) -> io::Result<()> { - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - visit_dirs(&path, cb)?; - } else { - cb(&entry); - } - } - } else { - panic!("Not a directory: {:?}", dir); - } - Ok(()) -} - -fn discover_all_tests() -> Vec { - let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let assets_root = workspace_root - .join("..") - .join("..") - .join("..") - .join("assets"); - let test_root = assets_root.join("tests"); - let mut test_files = Vec::new(); - visit_dirs(&test_root, &mut |entry| { - let path = entry.path(); - if path.extension().unwrap() == "rhai" { - let relative = path.strip_prefix(&assets_root).unwrap(); - test_files.push(Test { - path: relative.to_path_buf(), - }); - } - }) - .unwrap(); - - test_files -} - -// run this with `cargo test --features lua54 --package bevy_mod_scripting_lua --test lua_tests` -// or filter using the prefix "lua test -" -fn main() { - // Parse command line arguments - let args = Arguments::from_args(); - - // Create a list of tests and/or benchmarks (in this case: two dummy tests). - let tests = discover_all_tests() - .into_iter() - .map(|t| Trial::test(t.name(), move || t.execute())); - - // Run all tests and exit the application appropriatly. - libtest_mimic::run(&args, tests.collect()).exit(); -} diff --git a/crates/testing_crates/script_integration_test_harness/Cargo.toml b/crates/testing_crates/script_integration_test_harness/Cargo.toml index 850c776797..3e7111d35b 100644 --- a/crates/testing_crates/script_integration_test_harness/Cargo.toml +++ b/crates/testing_crates/script_integration_test_harness/Cargo.toml @@ -4,6 +4,11 @@ version = "0.1.0" edition = "2021" publish = false +[features] +default = ["lua", "rhai"] +lua = ["bevy_mod_scripting_lua", "bevy_mod_scripting_functions/lua_bindings"] +rhai = ["bevy_mod_scripting_rhai", "bevy_mod_scripting_functions/rhai_bindings"] + [dependencies] bevy = { workspace = true } test_utils = { workspace = true } @@ -11,8 +16,9 @@ bevy_mod_scripting_core = { workspace = true } bevy_mod_scripting_functions = { workspace = true, features = [ "bevy_bindings", "core_functions", - "rhai_bindings", - "lua_bindings", ] } regex = { version = "1.11" } pretty_assertions = "1.*" +bevy_mod_scripting_lua = { path = "../../languages/bevy_mod_scripting_lua", optional = true } +bevy_mod_scripting_rhai = { path = "../../languages/bevy_mod_scripting_rhai", optional = true } +criterion = "0.5" diff --git a/crates/testing_crates/script_integration_test_harness/src/lib.rs b/crates/testing_crates/script_integration_test_harness/src/lib.rs index d17836bbc4..2078cf2ef4 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -7,13 +7,13 @@ use std::{ }; use bevy::{ - app::{App, Last, PostUpdate, Startup, Update}, + app::{Last, Plugin, PostUpdate, Startup, Update}, asset::{AssetServer, Handle}, ecs::{ event::{Event, Events}, schedule::{IntoSystemConfigs, SystemConfigs}, system::{IntoSystem, Local, Res, SystemState}, - world::Mut, + world::{FromWorld, Mut}, }, prelude::{Entity, World}, reflect::TypeRegistry, @@ -27,7 +27,7 @@ use bevy_mod_scripting_core::{ extractors::{HandlerContext, WithWorldGuard}, handler::handle_script_errors, script::ScriptId, - IntoScriptPluginParams, + IntoScriptPluginParams, ScriptingPlugin, }; use bevy_mod_scripting_functions::ScriptFunctionsPlugin; use test_functions::register_test_functions; @@ -38,6 +38,33 @@ fn dummy_startup_system() {} fn dummy_before_post_update_system() {} fn dummy_post_update_system() {} +pub trait Benchmarker: 'static + Send + Sync { + fn bench( + &self, + label: &str, + f: &dyn Fn() -> Result, + ) -> Result; + + fn clone_box(&self) -> Box; +} + +#[derive(Clone)] +pub struct NoOpBenchmarker; + +impl Benchmarker for NoOpBenchmarker { + fn bench( + &self, + _label: &str, + f: &dyn Fn() -> Result, + ) -> Result { + f() + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + #[derive(Event)] struct TestEventFinished; @@ -62,13 +89,124 @@ impl TestCallbackBuilder } } +#[cfg(feature = "lua")] +pub fn make_test_lua_plugin() -> bevy_mod_scripting_lua::LuaScriptingPlugin { + use bevy_mod_scripting_core::{bindings::WorldContainer, ConfigureScriptPlugin}; + use bevy_mod_scripting_lua::{mlua, LuaScriptingPlugin}; + + LuaScriptingPlugin::default().add_context_initializer( + |_, ctxt: &mut bevy_mod_scripting_lua::mlua::Lua| { + let globals = ctxt.globals(); + globals.set( + "assert_throws", + ctxt.create_function(|_lua, (f, reg): (mlua::Function, String)| { + let world = + bevy_mod_scripting_core::bindings::ThreadWorldContainer.try_get_world()?; + let result = f.call::<()>(mlua::MultiValue::new()); + let err = match result { + Ok(_) => { + return Err(mlua::Error::external( + "Expected function to throw error, but it did not.", + )) + } + Err(e) => ScriptError::from_mlua_error(e).display_with_world(world), + }; + + let regex = regex::Regex::new(®).unwrap(); + if regex.is_match(&err) { + Ok(()) + } else { + Err(mlua::Error::external(format!( + "Expected error message to match the regex: \n{}\n\nBut got:\n{}", + regex.as_str(), + err + ))) + } + })?, + )?; + Ok(()) + }, + ) +} + +#[cfg(feature = "rhai")] +pub fn make_test_rhai_plugin() -> bevy_mod_scripting_rhai::RhaiScriptingPlugin { + use bevy_mod_scripting_core::{ + bindings::{ThreadWorldContainer, WorldContainer}, + ConfigureScriptPlugin, + }; + use bevy_mod_scripting_rhai::{ + rhai::{Dynamic, EvalAltResult, FnPtr, NativeCallContext}, + RhaiScriptingPlugin, + }; + + RhaiScriptingPlugin::default().add_runtime_initializer(|runtime| { + let mut runtime = runtime.write(); + + runtime.register_fn("assert", |a: Dynamic, b: &str| { + if !a.is::() { + panic!("Expected a boolean value, but got {:?}", a); + } + if !a.as_bool().unwrap() { + panic!("Assertion failed. {}", b); + } + }); + + runtime.register_fn("assert", |a: Dynamic| { + if !a.is::() { + panic!("Expected a boolean value, but got {:?}", a); + } + if !a.as_bool().unwrap() { + panic!("Assertion failed"); + } + }); + runtime.register_fn( + "assert_throws", + |ctxt: NativeCallContext, fn_: FnPtr, regex: String| { + let world = ThreadWorldContainer.try_get_world()?; + let args: [Dynamic; 0] = []; + let result = fn_.call_within_context::<()>(&ctxt, args); + match result { + Ok(_) => panic!("Expected function to throw error, but it did not."), + Err(e) => { + let e = ScriptError::from_rhai_error(*e); + let err = e.display_with_world(world); + let regex = regex::Regex::new(®ex).unwrap(); + if regex.is_match(&err) { + Ok::<(), Box>(()) + } else { + panic!( + "Expected error message to match the regex: \n{}\n\nBut got:\n{}", + regex.as_str(), + err + ) + } + } + } + }, + ); + Ok(()) + }) +} + +#[cfg(feature = "lua")] +pub fn execute_lua_integration_test(script_id: &str) -> Result<(), String> { + let plugin = make_test_lua_plugin(); + execute_integration_test(plugin, |_, _| {}, script_id) +} + +#[cfg(feature = "rhai")] +pub fn execute_rhai_integration_test(script_id: &str) -> Result<(), String> { + let plugin = make_test_rhai_plugin(); + execute_integration_test(plugin, |_, _| {}, script_id) +} + pub fn execute_integration_test< - P: IntoScriptPluginParams, + P: IntoScriptPluginParams + Plugin + AsMut>, F: FnOnce(&mut World, &mut TypeRegistry), - G: FnOnce(&mut App), >( + plugin: P, init: F, - init_app: G, script_id: &str, ) -> Result<(), String> { // set "BEVY_ASSET_ROOT" to the global assets folder, i.e. CARGO_MANIFEST_DIR/../../../assets @@ -86,12 +224,10 @@ pub fn execute_integration_test< let mut app = setup_integration_test(init); - app.add_plugins(ScriptFunctionsPlugin); + app.add_plugins((ScriptFunctionsPlugin, plugin)); register_test_functions(&mut app); - init_app(&mut app); - app.add_event::(); callback_labels!( @@ -180,6 +316,7 @@ fn run_test_callback( vec![], guard.clone(), ); + let e = match res { Ok(ScriptValue::Error(e)) => e.into(), Err(e) => e, @@ -196,7 +333,132 @@ fn run_test_callback( } } }; + handle_script_errors(guard, vec![e.clone()].into_iter()); Err(e) } + +#[cfg(feature = "lua")] +pub fn run_lua_benchmark( + script_id: &str, + label: &str, + criterion: &mut criterion::BenchmarkGroup, +) -> Result<(), String> { + use bevy_mod_scripting_lua::mlua::Function; + + let plugin = make_test_lua_plugin(); + run_plugin_benchmark( + plugin, + script_id, + label, + criterion, + |ctxt, _runtime, label, criterion| { + let bencher: Function = ctxt.globals().get("bench").map_err(|e| e.to_string())?; + criterion.bench_function(label, |c| { + c.iter(|| { + bencher.call::<()>(()).unwrap(); + }) + }); + Ok(()) + }, + ) +} + +#[cfg(feature = "rhai")] +pub fn run_rhai_benchmark( + script_id: &str, + label: &str, + criterion: &mut criterion::BenchmarkGroup, +) -> Result<(), String> { + use bevy_mod_scripting_rhai::rhai::Dynamic; + + let plugin = make_test_rhai_plugin(); + run_plugin_benchmark( + plugin, + script_id, + label, + criterion, + |ctxt, runtime, label, criterion| { + let runtime = runtime.read(); + const ARGS: [usize; 0] = []; + criterion.bench_function(label, |c| { + c.iter(|| { + let _ = runtime + .call_fn::(&mut ctxt.scope, &ctxt.ast, "bench", ARGS) + .unwrap(); + }) + }); + Ok(()) + }, + ) +} + +pub fn run_plugin_benchmark( + plugin: P, + script_id: &str, + label: &str, + criterion: &mut criterion::BenchmarkGroup, + bench_fn: F, +) -> Result<(), String> +where + P: IntoScriptPluginParams + Plugin, + F: Fn(&mut P::C, &P::R, &str, &mut criterion::BenchmarkGroup) -> Result<(), String>, +{ + use bevy_mod_scripting_core::bindings::{ + ThreadWorldContainer, WorldAccessGuard, WorldContainer, + }; + + let mut app = setup_integration_test(|_, _| {}); + + app.add_plugins((ScriptFunctionsPlugin, plugin)); + register_test_functions(&mut app); + + let script_id = script_id.to_owned(); + let script_id_clone = script_id.clone(); + app.add_systems( + Startup, + move |server: Res, mut handle: Local>| { + *handle = server.load(script_id_clone.to_owned()); + }, + ); + + // finalize + app.cleanup(); + app.finish(); + + let timer = Instant::now(); + + let mut state = SystemState::>>::from_world(app.world_mut()); + + loop { + app.update(); + + let mut handler_ctxt = state.get_mut(app.world_mut()); + let (guard, context) = handler_ctxt.get_mut(); + + if context.is_script_fully_loaded(script_id.clone().into()) { + let script = context + .scripts() + .get_mut(script_id.to_owned()) + .ok_or_else(|| String::from("Could not find scripts resource"))?; + let ctxt_arc = script.context.clone(); + let mut ctxt_locked = ctxt_arc.lock(); + + let runtime = &context.runtime_container().runtime; + + return WorldAccessGuard::with_existing_static_guard(guard, |guard| { + // Ensure the world is available via ThreadWorldContainer + ThreadWorldContainer + .set_world(guard.clone()) + .map_err(|e| e.display_with_world(guard))?; + // Pass the locked context to the closure for benchmarking its Lua (or generic) part + bench_fn(&mut ctxt_locked, runtime, label, criterion) + }); + } + state.apply(app.world_mut()); + if timer.elapsed() > Duration::from_secs(30) { + return Err("Timeout after 30 seconds".into()); + } + } +} diff --git a/crates/testing_crates/test_utils/src/lib.rs b/crates/testing_crates/test_utils/src/lib.rs index 1b883431b8..ed79df1316 100644 --- a/crates/testing_crates/test_utils/src/lib.rs +++ b/crates/testing_crates/test_utils/src/lib.rs @@ -1,2 +1,75 @@ +use std::{ + fs::{self, DirEntry}, + io, + path::{Path, PathBuf}, +}; + pub mod test_data; pub mod test_plugin; + +#[derive(Debug, Clone, Copy)] +pub enum TestKind { + Lua, + Rhai, +} + +impl std::fmt::Display for TestKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TestKind::Lua => write!(f, "Lua"), + TestKind::Rhai => write!(f, "Rhai"), + } + } +} + +#[derive(Debug, Clone)] +pub struct Test { + pub path: PathBuf, + pub kind: TestKind, +} + +fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&DirEntry)) -> io::Result<()> { + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + visit_dirs(&path, cb)?; + } else { + cb(&entry); + } + } + } else { + panic!("Not a directory: {:?}", dir); + } + Ok(()) +} + +pub fn discover_all_tests(manifest_dir: PathBuf, filter: impl Fn(&Path) -> bool) -> Vec { + let assets_root = manifest_dir.join("assets"); + let mut test_files = Vec::new(); + visit_dirs(&assets_root, &mut |entry| { + let path = entry.path(); + if let Some(kind) = path + .extension() + .and_then(|e| match e.to_string_lossy().as_ref() { + "lua" => Some(TestKind::Lua), + "rhai" => Some(TestKind::Rhai), + _ => None, + }) + { + // only take the path from the assets bit + let relative = path.strip_prefix(&assets_root).unwrap(); + if !filter(relative) { + return; + } + test_files.push(Test { + path: relative.to_path_buf(), + kind, + }); + } + }) + .unwrap(); + + test_files +} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 35a5fb72e9..77713de1c9 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -812,29 +812,29 @@ impl Xtasks { args.push(command.to_owned()); - if command != "fmt" && command != "bevy-api-gen" && command != "run" && command != "install" - { + if command != "fmt" && command != "bevy-api-gen" && command != "install" { // fmt doesn't care about features, workspaces or profiles + if command != "run" { + args.push("--workspace".to_owned()); + + if let Some(profile) = app_settings.profile.as_ref() { + let use_profile = if profile == "ephemeral-build" && app_settings.coverage { + // use special profile for coverage as it needs debug information + // but also don't want it too slow + "ephemeral-coverage" + } else { + profile + }; + + if !app_settings.coverage { + args.push("--profile".to_owned()); + args.push(use_profile.to_owned()); + } - args.push("--workspace".to_owned()); - - if let Some(profile) = app_settings.profile.as_ref() { - let use_profile = if profile == "ephemeral-build" && app_settings.coverage { - // use special profile for coverage as it needs debug information - // but also don't want it too slow - "ephemeral-coverage" - } else { - profile - }; - - if !app_settings.coverage { - args.push("--profile".to_owned()); - args.push(use_profile.to_owned()); - } - - if let Some(jobs) = app_settings.jobs { - args.push("--jobs".to_owned()); - args.push(jobs.to_string()); + if let Some(jobs) = app_settings.jobs { + args.push("--jobs".to_owned()); + args.push(jobs.to_string()); + } } } diff --git a/tests/script_tests.rs b/tests/script_tests.rs new file mode 100644 index 0000000000..73c7c3995c --- /dev/null +++ b/tests/script_tests.rs @@ -0,0 +1,55 @@ +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, missing_docs)] + +use std::path::PathBuf; + +use libtest_mimic::{Arguments, Failed, Trial}; +use script_integration_test_harness::{ + execute_lua_integration_test, execute_rhai_integration_test, +}; + +use test_utils::{discover_all_tests, Test, TestKind}; + +trait TestExecutor { + fn execute(self) -> Result<(), Failed>; + fn name(&self) -> String; +} + +impl TestExecutor for Test { + fn execute(self) -> Result<(), Failed> { + println!("Running test: {:?}", self.path); + + match self.kind { + TestKind::Lua => execute_lua_integration_test(&self.path.to_string_lossy())?, + TestKind::Rhai => execute_rhai_integration_test(&self.path.to_string_lossy())?, + } + + Ok(()) + } + + fn name(&self) -> String { + format!( + "script_test - {} - {}", + self.kind, + self.path + .to_string_lossy() + .split(&format!("tests{}data", std::path::MAIN_SEPARATOR)) + .last() + .unwrap() + ) + } +} + +// run this with `cargo test --features lua54 --package bevy_mod_scripting_lua --test lua_tests` +// or filter using the prefix "lua test -" +fn main() { + // Parse command line arguments + let args = Arguments::from_args(); + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + let tests = discover_all_tests(manifest_dir, |p| p.starts_with("tests")) + .into_iter() + .map(|t| Trial::test(t.name(), move || t.execute())) + .collect::>(); + + libtest_mimic::run(&args, tests).exit(); +}