diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7788624..bcf4f82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,3 +60,12 @@ jobs: - run: "mise clippy" - run: "mise bevy_lint" - run: "mise test" + + - uses: taiki-e/install-action@cargo-llvm-cov + - name: Collect coverage data + run: cargo llvm-cov nextest + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: ada-x64/qproj diff --git a/.zed/settings.json b/.zed/settings.json index 210d235..f101d92 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -3,18 +3,19 @@ "rust-analyzer": { "initialization_options": { "cargo": { - "features": "all" + "features": "all", }, "check": { "command": "clippy", - "extraArgs": ["--locked", "--target-dir=target/rust-analyzer"] - } - } - } + "extraArgs": ["--locked", "--target-dir=target/rust-analyzer"], + "workspace": false, + }, + }, + }, }, "languages": { "Python": { - "language_servers": ["ruff", "basedpyright"] - } - } + "language_servers": ["ruff", "basedpyright"], + }, + }, } diff --git a/Cargo.lock b/Cargo.lock index ea9b266..13da645 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5587,6 +5587,7 @@ dependencies = [ "bevy", "bevy-inspector-egui", "bevy_asset_loader", + "bevy_test_harness", "bitflags 2.10.0", "itertools 0.14.0", "serde", @@ -7045,18 +7046,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" +checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" +checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22" dependencies = [ "proc-macro2", "quote", diff --git a/crates/tfw/Cargo.toml b/crates/tfw/Cargo.toml index aaa8e59..33d8e3e 100644 --- a/crates/tfw/Cargo.toml +++ b/crates/tfw/Cargo.toml @@ -18,6 +18,9 @@ anyhow = "1.0.99" strum = { version = "0.27.2", features = ["derive"] } variadics_please = "1.1.0" +# local +bevy_test_harness = {path="../test_harness"} + [dev-dependencies] # just for a few inspector things... should really fork them bevy-inspector-egui = "0.36" diff --git a/crates/tfw/tests/app/mod.rs b/crates/tfw/tests/app/mod.rs index 0d5087b..7f3d2b0 100644 --- a/crates/tfw/tests/app/mod.rs +++ b/crates/tfw/tests/app/mod.rs @@ -1,12 +1,10 @@ pub mod screen; pub mod service; -pub mod util; pub mod prelude { pub use super::AppPlugin; pub use super::screen::prelude::*; pub use super::service::prelude::*; - pub use super::util::prelude::*; pub use bevy::prelude::*; pub use tfw::prelude::*; } diff --git a/crates/tfw/tests/app/util/app_ext.rs b/crates/tfw/tests/app/util/app_ext.rs deleted file mode 100644 index d235ea6..0000000 --- a/crates/tfw/tests/app/util/app_ext.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::prelude::*; - -pub trait AppExt { - fn add_step(&mut self, step: u32, system: impl IntoSystem<(), (), M>) -> &mut Self; -} -impl AppExt for App { - /// Registers a system which runs in PostUpdate (after all screen events have occured). - /// Will only run if the state is set to the specified value. - fn add_step(&mut self, step: u32, system: impl IntoSystem<(), (), M>) -> &mut Self { - self.add_systems( - PostUpdate, - system.run_if(in_state(Step(step))).after(log_step), - ) - } -} diff --git a/crates/tfw/tests/app/util/commands_ext.rs b/crates/tfw/tests/app/util/commands_ext.rs deleted file mode 100644 index 5f2b53b..0000000 --- a/crates/tfw/tests/app/util/commands_ext.rs +++ /dev/null @@ -1,123 +0,0 @@ -use bevy::ecs::query::QueryFilter; -use bevy_inspector_egui::bevy_inspector::{ - guess_entity_name, - hierarchy::{Hierarchy, SelectedEntities}, -}; - -use crate::prelude::*; - -pub trait CommandsExt { - fn log_hierarchy(&mut self); - fn find_entity(&mut self, name: impl ToString); - fn find_no_entity(&mut self, name: impl ToString); - fn find_entity_filtered(&mut self, name: impl ToString); - fn find_no_entity_filtered(&mut self, name: impl ToString); - fn find_entity_with( - &mut self, - name: impl ToString, - value: C, - invert: bool, - ); -} -impl<'w, 's> CommandsExt for Commands<'w, 's> { - fn log_hierarchy(&mut self) { - self.run_system_cached(|world: &mut World| { - let h = Hierarchy { - world, - selected: &mut SelectedEntities::default(), - context_menu: None, - shortcircuit_entity: None, - extra_state: &mut (), - }; - - let mut root_query = h.world.query_filtered::>(); - let entities: Vec<_> = root_query.iter(h.world).collect(); - let mut output = String::new(); - log_hierarchy_inner(world, &mut output, entities, 0); - info!("{output}") - }); - } - fn find_entity(&mut self, name: impl ToString) { - self.run_system_cached_with(find_entity, (name.to_string(), false)); - } - fn find_no_entity(&mut self, name: impl ToString) { - self.run_system_cached_with(find_entity, (name.to_string(), true)); - } - fn find_entity_filtered(&mut self, name: impl ToString) { - self.run_system_cached_with(find_entity_filtered::, (name.to_string(), false)); - } - fn find_no_entity_filtered(&mut self, name: impl ToString) { - self.run_system_cached_with(find_entity_filtered::, (name.to_string(), true)); - } - fn find_entity_with( - &mut self, - name: impl ToString, - value: C, - invert: bool, - ) { - self.run_system_cached_with(find_entity_with, (name.to_string(), invert, value)); - } -} - -fn log_hierarchy_inner(world: &mut World, output: &mut String, entities: Vec, depth: u32) { - for &entity in &entities { - let entity_name = guess_entity_name(world, entity); - let mut tags = vec![]; - if world.entity(entity).get::().is_some() { - tags.push("Persistent"); - } - if world.entity(entity).get::().is_some() { - tags.push("Observer"); - } - let indent = (0..depth).map(|_| "-").collect::>().join(""); - #[allow(clippy::obfuscated_if_else)] - let tags = (!tags.is_empty()) - .then(|| format!("<{}>", tags.join(", "))) - .unwrap_or_default(); - - *output = format!("{output}\n{indent}> {entity_name} {tags}"); - - if let Some(children) = world.entity(entity).get::() { - let children = children.iter().collect::>(); - log_hierarchy_inner(world, output, children, depth + 1); - } - } -} - -/// Searches for an entity with the given [Name] component. -/// This _will not_ show entities marked with [Internal], including Observers. -fn find_entity(input: In<(String, bool)>, q: Query<&Name>, mut commands: Commands) { - let (name, invert) = input.0; - let any = q.iter().any(|ename| (**ename).eq(&name)); - if (invert && any) || (!invert && !any) { - commands.write_message(AppExit::error()); - } -} - -/// Searches for an entity with the given [Name] component -fn find_entity_filtered( - input: In<(String, bool)>, - q: Query<&Name, F>, - mut commands: Commands, -) { - let (name, invert) = input.0; - let any = q.iter().any(|ename| (**ename).eq(&name)); - if (invert && any) || (!invert && !any) { - commands.write_message(AppExit::error()); - } -} - -/// Searches for an entity with the given [Name] and component C. -fn find_entity_with( - input: In<(String, bool, C)>, - q: Query<(&Name, &C)>, - mut commands: Commands, -) { - let (name, invert, value) = input.0; - let any = q - .iter() - .any(|(ename, c)| (**ename).eq(&name) && *c == value); - if (invert && any) || (!invert && !any) { - commands.write_message(AppExit::error()); - } -} diff --git a/crates/tfw/tests/app/util/data.rs b/crates/tfw/tests/app/util/data.rs deleted file mode 100644 index 121ded9..0000000 --- a/crates/tfw/tests/app/util/data.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::prelude::*; - -#[derive(States, Debug, Default, Deref, DerefMut, Clone, Copy, Hash, Eq, PartialEq)] -pub struct Step(pub u32); diff --git a/crates/tfw/tests/app/util/mod.rs b/crates/tfw/tests/app/util/mod.rs deleted file mode 100644 index 1c4c3a2..0000000 --- a/crates/tfw/tests/app/util/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod app_ext; -mod commands_ext; -mod data; -mod runner; - -pub mod prelude { - pub use super::app_ext::*; - pub use super::commands_ext::*; - pub use super::data::*; - pub use super::runner::*; -} diff --git a/crates/tfw/tests/app/util/runner.rs b/crates/tfw/tests/app/util/runner.rs deleted file mode 100644 index ebfb1cc..0000000 --- a/crates/tfw/tests/app/util/runner.rs +++ /dev/null @@ -1,131 +0,0 @@ -use bevy::{ - app::ScheduleRunnerPlugin, - core_pipeline::CorePipelinePlugin, - diagnostic::FrameCountPlugin, - log::LogPlugin, - mesh::MeshPlugin, - pbr::PbrPlugin, - prelude::*, - render::{ - RenderPlugin, - settings::{RenderCreation, WgpuSettings}, - }, - scene::ScenePlugin, - state::app::StatesPlugin, - time::TimePlugin, - window::ExitCondition, -}; - -use crate::prelude::*; - -pub trait TestFn: Fn(&mut App) {} -impl TestFn for T where T: Fn(&mut App) {} - -#[derive(Debug)] -pub struct TestRunnerPlugin { - pub timeout: f32, -} -impl Default for TestRunnerPlugin { - fn default() -> Self { - Self { timeout: 5. } // 5 sec - } -} -impl Plugin for TestRunnerPlugin { - fn build(&self, app: &mut App) { - app.add_plugins(( - TaskPoolPlugin::default(), - FrameCountPlugin, - TimePlugin, - ScheduleRunnerPlugin::default(), - LogPlugin::default(), - WindowPlugin { - primary_window: Some(Window { - title: "Headless test".into(), - resizable: true, - focused: true, - visible: false, - desired_maximum_frame_latency: None, - ..Default::default() - }), - primary_cursor_options: None, - close_when_requested: true, - exit_condition: ExitCondition::OnPrimaryClosed, - }, - AssetPlugin::default(), - RenderPlugin { - render_creation: RenderCreation::Automatic(WgpuSettings { - backends: None, - ..Default::default() - }), - ..Default::default() - }, - ImagePlugin::default(), - CorePipelinePlugin, - MeshPlugin, - PbrPlugin { - prepass_enabled: false, - add_default_deferred_lighting_plugin: false, - use_gpu_instance_buffer_builder: false, - ..Default::default() - }, - StatesPlugin, - ScenePlugin, - )); - - let timeout = self.timeout; - app.add_systems( - Update, - move |time: Res>, mut events: MessageWriter| { - let elapsed = time.elapsed_secs(); - if elapsed > timeout { - error!("Timeout after {elapsed}s"); - events.write(AppExit::error()); - } - }, - ); - app.add_systems(PostUpdate, (log_step, check_exit).chain()); - - app.init_state::(); - } -} - -pub fn log_step(step: Res>, mut local_step: Local) { - info_once!("Step = {}", ***step); // when step = 0 - if ***step != *local_step { - *local_step = ***step; - info!("Step = {}", ***step); - } -} - -fn check_exit(mut reader: MessageReader) { - for msg in reader.read() { - error!("Obtained exit message {msg:?}"); - } -} - -#[test] -fn timeout() { - let mut app = App::new(); - app.add_plugins(TestRunnerPlugin { timeout: 0.5 }); - assert!(app.run().is_error()); -} - -#[test] -fn explicit_failure() { - let mut app = App::new(); - app.add_plugins(TestRunnerPlugin::default()); - app.add_systems(First, |mut commands: Commands| { - commands.write_message(AppExit::error()); - }); - assert!(app.run().is_error()); -} - -#[test] -fn explicit_success() { - let mut app = App::new(); - app.add_plugins(TestRunnerPlugin::default()); - app.add_systems(First, |mut commands: Commands| { - commands.write_message(AppExit::Success); - }); - assert!(app.run().is_success()); -} diff --git a/crates/tfw/tests/screen/entity_scoping.rs b/crates/tfw/tests/screen/entity_scoping.rs index 124e344..6d1a588 100644 --- a/crates/tfw/tests/screen/entity_scoping.rs +++ b/crates/tfw/tests/screen/entity_scoping.rs @@ -1,3 +1,4 @@ +use bevy_test_harness::prelude::*; use tfw::{TfwPlugin, TfwSettings}; use crate::prelude::*; diff --git a/crates/tfw/tests/screen/mod.rs b/crates/tfw/tests/screen/mod.rs index 819b6b8..5c411a2 100644 --- a/crates/tfw/tests/screen/mod.rs +++ b/crates/tfw/tests/screen/mod.rs @@ -2,6 +2,7 @@ mod entity_scoping; mod system_scoping; use crate::prelude::*; +use bevy_test_harness::TestRunnerPlugin; use tfw::{TfwPlugin, TfwSettings}; // the rest of the tests use non-blocking asset loading to circumvent the delay. diff --git a/crates/tfw/tests/screen/system_scoping.rs b/crates/tfw/tests/screen/system_scoping.rs index e07be66..e0443fc 100644 --- a/crates/tfw/tests/screen/system_scoping.rs +++ b/crates/tfw/tests/screen/system_scoping.rs @@ -1,3 +1,4 @@ +use bevy_test_harness::prelude::*; use tfw::{TfwPlugin, TfwSettings}; use crate::prelude::*;