diff --git a/Cargo.toml b/Cargo.toml index 6613ec3c4e344..470c0dc195f42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ members = [ "tools/*", # Bevy's error codes. This is a crate so we can automatically check all of the code blocks. "errors", + # Integration tests that show how a user can test bevy systems/applications. + # This is a separate crate to make sure it purely uses the public interface. + "integration_tests", ] [workspace.lints.clippy] diff --git a/examples/README.md b/examples/README.md index 2ff4e504e1672..45264ef44f9ad 100644 --- a/examples/README.md +++ b/examples/README.md @@ -555,7 +555,7 @@ Example | Description Example | Description --- | --- -[How to Test Apps](../tests/how_to_test_apps.rs) | How to test apps (simple integration testing) +[How to Test Apps](../integration_tests/src/how_to_test_apps.rs) | How to test apps (simple integration testing) [How to Test Systems](../tests/how_to_test_systems.rs) | How to test systems with commands, queries or resources # Platform-Specific Examples diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml new file mode 100644 index 0000000000000..19b00971bccef --- /dev/null +++ b/integration_tests/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "integration_tests" +version = "0.1.0" +edition = "2021" +description = """Contains bevy's integration tests. +Is separate from the main bevy crate, so it doesn't impact the compilation time of examples. +And only uses the public bevy interface.""" +publish = false +license = "MIT OR Apache-2.0" + +[dev-dependencies] +bevy = { path = ".." } +postcard = { version = "1.0", features = ["alloc"] } +bincode = "1.3" +rmp-serde = "1.1" +ron = "0.8.0" +serde = { version = "1", features = ["derive"] } diff --git a/tests/how_to_test_apps.rs b/integration_tests/src/how_to_test_apps.rs similarity index 99% rename from tests/how_to_test_apps.rs rename to integration_tests/src/how_to_test_apps.rs index 441c81082a80f..d6af71b6eb70d 100644 --- a/tests/how_to_test_apps.rs +++ b/integration_tests/src/how_to_test_apps.rs @@ -1,3 +1,4 @@ +#![cfg(test)] //! Demonstrates simple integration testing of Bevy applications. //! //! By substituting [`DefaultPlugins`] with [`MinimalPlugins`], Bevy can run completely headless. diff --git a/tests/how_to_test_systems.rs b/integration_tests/src/how_to_test_systems.rs similarity index 91% rename from tests/how_to_test_systems.rs rename to integration_tests/src/how_to_test_systems.rs index a2cb86a680dfe..6610b6c9cf73b 100644 --- a/tests/how_to_test_systems.rs +++ b/integration_tests/src/how_to_test_systems.rs @@ -1,4 +1,4 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +#![cfg(test)] use bevy::prelude::*; #[derive(Component, Default)] @@ -109,8 +109,8 @@ fn did_despawn_enemy() { // Get `EnemyDied` event reader let enemy_died_events = app.world().resource::>(); - let mut enemy_died_reader = enemy_died_events.get_cursor(); - let enemy_died = enemy_died_reader.read(enemy_died_events).next().unwrap(); + let mut enemy_died_cursor = enemy_died_events.get_cursor(); + let enemy_died = enemy_died_cursor.read(enemy_died_events).next().unwrap(); // Check the event has been sent assert_eq!(enemy_died.0, 1); @@ -133,7 +133,10 @@ fn spawn_enemy_using_input_resource() { app.update(); // Check resulting changes, one entity has been spawned with `Enemy` component - assert_eq!(app.world_mut().query::<&Enemy>().iter(app.world()).len(), 1); + assert_eq!( + app.world_mut().query::<&Enemy>().iter(&app.world()).len(), + 1 + ); // Clear the `just_pressed` status for all `KeyCode`s app.world_mut() @@ -144,7 +147,10 @@ fn spawn_enemy_using_input_resource() { app.update(); // Check resulting changes, no new entity has been spawned - assert_eq!(app.world_mut().query::<&Enemy>().iter(app.world()).len(), 1); + assert_eq!( + app.world_mut().query::<&Enemy>().iter(&app.world()).len(), + 1 + ); } #[test] diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs new file mode 100644 index 0000000000000..a3b1eabd27527 --- /dev/null +++ b/integration_tests/src/lib.rs @@ -0,0 +1,3 @@ +mod how_to_test_apps; +mod how_to_test_systems; +mod scenes; diff --git a/integration_tests/src/scenes/components.rs b/integration_tests/src/scenes/components.rs new file mode 100644 index 0000000000000..4b15f62443b8c --- /dev/null +++ b/integration_tests/src/scenes/components.rs @@ -0,0 +1,186 @@ +#![cfg(test)] +//! Testing of serialization and deserialization of diverse scene components. + +use bevy::ecs::entity::EntityHashMap; +use bevy::prelude::*; +use bevy::reflect::erased_serde::__private::serde::de::DeserializeSeed; +use bevy::reflect::erased_serde::__private::serde::Serialize; +use bevy::reflect::TypeRegistry; +use bevy::scene::serde::{SceneDeserializer, SceneSerializer}; +use bevy::scene::ScenePlugin; +use bincode::Options; +use std::io::BufReader; + +#[test] +fn ron_roundtrip_equality() { + assert_round_trip_equality(serialize_world_ron, deserialize_world_ron); +} + +#[test] +fn postcard_roundtrip_equality() { + assert_round_trip_equality(serialize_world_postcard, deserialize_world_postcard); +} + +#[test] +fn bincode_roundtrip_equality() { + assert_round_trip_equality(serialize_world_bincode, deserialize_world_bincode); +} + +#[test] +fn messagepack_roundtrip_equality() { + assert_round_trip_equality(serialize_world_messagepack, deserialize_world_messagepack); +} + +/// Convenience function for testing the roundtrip equality of different serialization methods. +fn assert_round_trip_equality( + serialize: fn(DynamicScene, &TypeRegistry) -> Vec, + deserialize: fn(SceneDeserializer, &[u8]) -> DynamicScene, +) { + let mut input_app = create_test_app(); + spawn_test_entities(&mut input_app); + + let type_registry = input_app.world().resource::().read(); + let scene = DynamicScene::from_world(&input_app.world()); + let serialized = serialize(scene, &type_registry); + + // We use a clean app to deserialize into, so nothing of the input app can interfere. + let mut output_app = create_test_app(); + let scene = { + let scene_deserializer = SceneDeserializer { + type_registry: &output_app.world().resource::().read(), + }; + deserialize(scene_deserializer, &serialized) + }; + + let mut entity_map = EntityHashMap::default(); + scene + .write_to_world(&mut output_app.world_mut(), &mut entity_map) + .unwrap_or_else(|error| panic!("Could not add deserialized scene to world: {error}")); + + // TODO: Ideally we'd check whether the input and output world are exactly equal. But the world does not implement Eq. + // so instead we check the serialized outputs against each other. However, this will miss anything that fails to serialize in the first place. + + let type_registry = input_app.world().resource::().read(); + let scene = DynamicScene::from_world(&input_app.world()); + let serialized_again = serialize(scene, &type_registry); + + assert_eq!(serialized, serialized_again); +} + +fn serialize_world_ron(scene: DynamicScene, type_registry: &TypeRegistry) -> Vec { + scene + .serialize(type_registry) + .map(|output| output.as_bytes().to_vec()) + .unwrap_or_else(|error| panic!("Scene failed to serialize: {error}")) +} + +fn deserialize_world_ron(scene_deserializer: SceneDeserializer, input: &[u8]) -> DynamicScene { + let mut deserializer = ron::de::Deserializer::from_bytes(input) + .unwrap_or_else(|error| panic!("Scene failed to deserialize: {error}")); + scene_deserializer + .deserialize(&mut deserializer) + .unwrap_or_else(|error| panic!("Scene failed to deserialize: {error}")) +} + +fn serialize_world_postcard(scene: DynamicScene, type_registry: &TypeRegistry) -> Vec { + let scene_serializer = SceneSerializer::new(&scene, type_registry); + postcard::to_allocvec(&scene_serializer) + .unwrap_or_else(|error| panic!("Scene failed to serialize: {error}")) +} + +fn deserialize_world_postcard(scene_deserializer: SceneDeserializer, input: &[u8]) -> DynamicScene { + let mut deserializer = postcard::Deserializer::from_bytes(input); + scene_deserializer + .deserialize(&mut deserializer) + .unwrap_or_else(|error| panic!("Scene failed to deserialize: {error}")) +} + +fn serialize_world_bincode(scene: DynamicScene, type_registry: &TypeRegistry) -> Vec { + let scene_serializer = SceneSerializer::new(&scene, type_registry); + bincode::serialize(&scene_serializer) + .unwrap_or_else(|error| panic!("Scene failed to serialize: {error}")) +} + +fn deserialize_world_bincode(scene_deserializer: SceneDeserializer, input: &[u8]) -> DynamicScene { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .deserialize_seed(scene_deserializer, input) + .unwrap_or_else(|error| panic!("Scene failed to deserialize: {error}")) +} + +fn serialize_world_messagepack(scene: DynamicScene, type_registry: &TypeRegistry) -> Vec { + let scene_serializer = SceneSerializer::new(&scene, type_registry); + let mut buf = Vec::new(); + let mut ser = rmp_serde::Serializer::new(&mut buf); + scene_serializer + .serialize(&mut ser) + .unwrap_or_else(|error| panic!("Scene failed to serialize: {error}")); + buf +} + +fn deserialize_world_messagepack( + scene_deserializer: SceneDeserializer, + input: &[u8], +) -> DynamicScene { + let mut reader = BufReader::new(input); + + scene_deserializer + .deserialize(&mut rmp_serde::Deserializer::new(&mut reader)) + .unwrap_or_else(|error| panic!("Scene failed to deserialize: {error}")) +} + +fn create_test_app() -> App { + let mut app = App::new(); + app.add_plugins(MinimalPlugins).add_plugins(( + AssetPlugin::default(), + ScenePlugin, + TransformPlugin, + )); + + app +} + +fn spawn_test_entities(app: &mut App) { + let entity_1 = app.world_mut().spawn(Transform::default()).id(); + app.world_mut().spawn(ReferenceComponent(Some(entity_1))); + + app.world_mut() + .spawn(VectorComponent { + integer_vector: vec![1, 2, 3, 4, 5, 123456789], + // Testing different characters in strings + string_vector: vec![ + // Basic string + "Hello World!".to_string(), + // Common special characters + "!@#$%^&*(){}[]-=_+\\|,.<>/?;:'\"`~".to_string(), + // Emoji + "😄🌲🕊️🐧🏁".to_string(), + // Chinese characters + "月亮太阳".to_string(), + // Non-breaking space. + " ".to_string(), + ], + }) + .insert(TupleComponent( + (-12, "A tuple".to_string(), 2.345), + (true, false, 0), + )); +} + +/// Tests if Entity ids survive serialization. +#[derive(Component, Reflect, Default)] +#[reflect(Component)] +struct ReferenceComponent(Option); + +/// Tests if vectors survive serialization. +#[derive(Component, Reflect, Default)] +#[reflect(Component)] +struct VectorComponent { + integer_vector: Vec, + string_vector: Vec, +} + +/// Tests if tuples survive serialization. +#[derive(Component, Reflect, Default)] +#[reflect(Component)] +struct TupleComponent((i32, String, f32), (bool, bool, u32)); diff --git a/integration_tests/src/scenes/mod.rs b/integration_tests/src/scenes/mod.rs new file mode 100644 index 0000000000000..11cce97ae8fb2 --- /dev/null +++ b/integration_tests/src/scenes/mod.rs @@ -0,0 +1,2 @@ +#![cfg(test)] +mod components;