Skip to content

Add beginning of complex serialization / deserialization tests (#6834) #7336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions integration_tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![cfg(test)]
//! Demonstrates simple integration testing of Bevy applications.
//!
//! By substituting [`DefaultPlugins`] with [`MinimalPlugins`], Bevy can run completely headless.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg(test)]
use bevy::prelude::*;

#[derive(Component, Default)]
Expand Down Expand Up @@ -109,8 +109,8 @@ fn did_despawn_enemy() {

// Get `EnemyDied` event reader
let enemy_died_events = app.world().resource::<Events<EnemyDied>>();
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);
Expand All @@ -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()
Expand All @@ -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]
Expand Down
3 changes: 3 additions & 0 deletions integration_tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod how_to_test_apps;
mod how_to_test_systems;
mod scenes;
186 changes: 186 additions & 0 deletions integration_tests/src/scenes/components.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
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::<AppTypeRegistry>().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::<AppTypeRegistry>().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::<AppTypeRegistry>().read();
let scene = DynamicScene::from_world(&input_app.world());
let serialized_again = serialize(scene, &type_registry);

assert_eq!(serialized, serialized_again);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you can also check the size of entity_map after the call to write_to_world?

}

fn serialize_world_ron(scene: DynamicScene, type_registry: &TypeRegistry) -> Vec<u8> {
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<u8> {
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<u8> {
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<u8> {
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<Entity>);

/// Tests if vectors survive serialization.
#[derive(Component, Reflect, Default)]
#[reflect(Component)]
struct VectorComponent {
integer_vector: Vec<u32>,
string_vector: Vec<String>,
}

/// Tests if tuples survive serialization.
#[derive(Component, Reflect, Default)]
#[reflect(Component)]
struct TupleComponent((i32, String, f32), (bool, bool, u32));
2 changes: 2 additions & 0 deletions integration_tests/src/scenes/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#![cfg(test)]
mod components;
Loading