From 3087b0c996dbd3fb9a1dbcac4bf5c32f992c2e5e Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 8 Jun 2025 22:46:26 -0330 Subject: add support for panicking on warn/error in simulation tests --- azalea-client/src/lib.rs | 2 +- azalea-client/src/plugins/join.rs | 3 +- azalea-client/src/test_simulation.rs | 296 ----------------------------- azalea-client/src/test_utils/mod.rs | 6 + azalea-client/src/test_utils/simulation.rs | 296 +++++++++++++++++++++++++++++ azalea-client/src/test_utils/tracing.rs | 38 ++++ 6 files changed, 343 insertions(+), 298 deletions(-) delete mode 100644 azalea-client/src/test_simulation.rs create mode 100644 azalea-client/src/test_utils/mod.rs create mode 100644 azalea-client/src/test_utils/simulation.rs create mode 100644 azalea-client/src/test_utils/tracing.rs (limited to 'azalea-client/src') diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index 6bff353e..df04a606 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -17,7 +17,7 @@ pub mod player; mod plugins; #[doc(hidden)] -pub mod test_simulation; +pub mod test_utils; pub use account::{Account, AccountOpts}; pub use azalea_protocol::common::client_information::ClientInformation; diff --git a/azalea-client/src/plugins/join.rs b/azalea-client/src/plugins/join.rs index 09eeff59..8d094b7d 100644 --- a/azalea-client/src/plugins/join.rs +++ b/azalea-client/src/plugins/join.rs @@ -3,6 +3,7 @@ use std::{net::SocketAddr, sync::Arc}; use azalea_entity::{LocalEntity, indexing::EntityUuidIndex}; use azalea_protocol::{ ServerAddress, + common::client_information::ClientInformation, connect::{Connection, ConnectionError, Proxy}, packets::{ ClientIntention, ConnectionProtocol, PROTOCOL_VERSION, @@ -215,7 +216,7 @@ pub fn poll_create_connection_task( write_conn, ConnectionProtocol::Login, ), - client_information: crate::ClientInformation::default(), + client_information: ClientInformation::default(), instance_holder, metadata: azalea_entity::metadata::PlayerMetadataBundle::default(), }, diff --git a/azalea-client/src/test_simulation.rs b/azalea-client/src/test_simulation.rs deleted file mode 100644 index c53a624a..00000000 --- a/azalea-client/src/test_simulation.rs +++ /dev/null @@ -1,296 +0,0 @@ -use std::{fmt::Debug, sync::Arc}; - -use azalea_auth::game_profile::GameProfile; -use azalea_block::BlockState; -use azalea_buf::AzaleaWrite; -use azalea_core::{ - delta::PositionDelta8, - game_type::{GameMode, OptionalGameType}, - position::{ChunkPos, Vec3}, - resource_location::ResourceLocation, - tick::GameTick, -}; -use azalea_entity::metadata::PlayerMetadataBundle; -use azalea_protocol::packets::{ - ConnectionProtocol, Packet, ProtocolPacket, - common::CommonPlayerSpawnInfo, - config::{ClientboundFinishConfiguration, ClientboundRegistryData}, - game::{ - ClientboundAddEntity, ClientboundLevelChunkWithLight, ClientboundLogin, ClientboundRespawn, - c_level_chunk_with_light::ClientboundLevelChunkPacketData, - c_light_update::ClientboundLightUpdatePacketData, - }, -}; -use azalea_registry::{Biome, DimensionType, EntityKind}; -use azalea_world::{Chunk, Instance, MinecraftEntityId, Section, palette::PalettedContainer}; -use bevy_app::App; -use bevy_ecs::{component::Mutable, prelude::*, schedule::ExecutorKind}; -use parking_lot::RwLock; -use simdnbt::owned::{NbtCompound, NbtTag}; -use uuid::Uuid; - -use crate::{ - ClientInformation, InConfigState, LocalPlayerBundle, connection::RawConnection, - disconnect::DisconnectEvent, local_player::InstanceHolder, player::GameProfileComponent, -}; - -/// A way to simulate a client in a server, used for some internal tests. -pub struct Simulation { - pub app: App, - pub entity: Entity, - - // the runtime needs to be kept around for the tasks to be considered alive - pub rt: tokio::runtime::Runtime, -} - -impl Simulation { - pub fn new(initial_connection_protocol: ConnectionProtocol) -> Self { - let mut app = create_simulation_app(); - let mut entity = app.world_mut().spawn_empty(); - let (player, rt) = - create_local_player_bundle(entity.id(), ConnectionProtocol::Configuration); - entity.insert(player); - - let entity = entity.id(); - - tick_app(&mut app); - - // start in the config state - app.world_mut().entity_mut(entity).insert(( - InConfigState, - GameProfileComponent(GameProfile::new( - Uuid::from_u128(1234), - "azalea".to_string(), - )), - )); - tick_app(&mut app); - - let mut simulation = Self { app, entity, rt }; - - #[allow(clippy::single_match)] - match initial_connection_protocol { - ConnectionProtocol::Configuration => {} - ConnectionProtocol::Game => { - simulation.receive_packet(ClientboundRegistryData { - registry_id: ResourceLocation::new("minecraft:dimension_type"), - entries: vec![( - ResourceLocation::new("minecraft:overworld"), - Some(NbtCompound::from_values(vec![ - ("height".into(), NbtTag::Int(384)), - ("min_y".into(), NbtTag::Int(-64)), - ])), - )] - .into_iter() - .collect(), - }); - - simulation.receive_packet(ClientboundFinishConfiguration); - simulation.tick(); - } - _ => unimplemented!("unsupported ConnectionProtocol {initial_connection_protocol:?}"), - } - - simulation - } - - pub fn receive_packet(&mut self, packet: impl Packet

) { - let buf = azalea_protocol::write::serialize_packet(&packet.into_variant()).unwrap(); - self.with_component_mut::(|raw_conn| { - raw_conn.injected_clientbound_packets.push(buf); - }); - } - - pub fn tick(&mut self) { - tick_app(&mut self.app); - } - - pub fn minecraft_entity_id(&self) -> MinecraftEntityId { - self.component::() - } - - pub fn component(&self) -> T { - self.app.world().get::(self.entity).unwrap().clone() - } - pub fn get_component(&self) -> Option { - self.app.world().get::(self.entity).cloned() - } - pub fn has_component(&self) -> bool { - self.app.world().get::(self.entity).is_some() - } - pub fn with_component_mut>( - &mut self, - f: impl FnOnce(&mut T), - ) { - f(&mut self - .app - .world_mut() - .entity_mut(self.entity) - .get_mut::() - .unwrap()); - } - pub fn resource(&self) -> T { - self.app.world().get_resource::().unwrap().clone() - } - pub fn with_resource(&self, f: impl FnOnce(&T)) { - f(self.app.world().get_resource::().unwrap()); - } - pub fn with_resource_mut(&mut self, f: impl FnOnce(Mut)) { - f(self.app.world_mut().get_resource_mut::().unwrap()); - } - - pub fn chunk(&self, chunk_pos: ChunkPos) -> Option>> { - self.component::() - .instance - .read() - .chunks - .get(&chunk_pos) - } - - pub fn disconnect(&mut self) { - // send DisconnectEvent - self.app.world_mut().send_event(DisconnectEvent { - entity: self.entity, - reason: None, - }); - } -} - -#[allow(clippy::type_complexity)] -fn create_local_player_bundle( - entity: Entity, - connection_protocol: ConnectionProtocol, -) -> (LocalPlayerBundle, tokio::runtime::Runtime) { - // unused since we'll trigger ticks ourselves - - let rt = tokio::runtime::Runtime::new().unwrap(); - - let raw_connection = RawConnection::new_networkless(connection_protocol); - - let instance = Instance::default(); - let instance_holder = InstanceHolder::new(entity, Arc::new(RwLock::new(instance))); - - let local_player_bundle = LocalPlayerBundle { - raw_connection, - client_information: ClientInformation::default(), - instance_holder, - metadata: PlayerMetadataBundle::default(), - }; - - (local_player_bundle, rt) -} - -fn create_simulation_app() -> App { - let mut app = App::new(); - - #[cfg(feature = "log")] - app.add_plugins( - bevy_app::PluginGroup::build(crate::DefaultPlugins).disable::(), - ); - - app.edit_schedule(bevy_app::Main, |schedule| { - // makes test results more reproducible - schedule.set_executor_kind(ExecutorKind::SingleThreaded); - }); - app -} - -fn tick_app(app: &mut App) { - app.update(); - app.world_mut().run_schedule(GameTick); -} - -pub fn make_basic_login_packet( - dimension_type: DimensionType, - dimension: ResourceLocation, -) -> ClientboundLogin { - ClientboundLogin { - player_id: MinecraftEntityId(0), - hardcore: false, - levels: vec![], - max_players: 20, - chunk_radius: 8, - simulation_distance: 8, - reduced_debug_info: false, - show_death_screen: true, - do_limited_crafting: false, - common: CommonPlayerSpawnInfo { - dimension_type, - dimension, - seed: 0, - game_type: GameMode::Survival, - previous_game_type: OptionalGameType(None), - is_debug: false, - is_flat: false, - last_death_location: None, - portal_cooldown: 0, - sea_level: 63, - }, - enforces_secure_chat: false, - } -} - -pub fn make_basic_respawn_packet( - dimension_type: DimensionType, - dimension: ResourceLocation, -) -> ClientboundRespawn { - ClientboundRespawn { - common: CommonPlayerSpawnInfo { - dimension_type, - dimension, - seed: 0, - game_type: GameMode::Survival, - previous_game_type: OptionalGameType(None), - is_debug: false, - is_flat: false, - last_death_location: None, - portal_cooldown: 0, - sea_level: 63, - }, - data_to_keep: 0, - } -} - -pub fn make_basic_empty_chunk( - pos: ChunkPos, - section_count: usize, -) -> ClientboundLevelChunkWithLight { - let mut chunk_bytes = Vec::new(); - let mut sections = Vec::new(); - for _ in 0..section_count { - sections.push(Section { - block_count: 0, - states: PalettedContainer::::new(), - biomes: PalettedContainer::::new(), - }); - } - sections.azalea_write(&mut chunk_bytes).unwrap(); - - ClientboundLevelChunkWithLight { - x: pos.x, - z: pos.z, - chunk_data: ClientboundLevelChunkPacketData { - heightmaps: Default::default(), - data: Arc::new(chunk_bytes.into()), - block_entities: vec![], - }, - light_data: ClientboundLightUpdatePacketData::default(), - } -} - -pub fn make_basic_add_entity( - entity_type: EntityKind, - id: i32, - position: impl Into, -) -> ClientboundAddEntity { - ClientboundAddEntity { - id: id.into(), - uuid: Uuid::from_u128(1234), - entity_type, - position: position.into(), - x_rot: 0, - y_rot: 0, - y_head_rot: 0, - data: 0, - velocity: PositionDelta8::default(), - } -} diff --git a/azalea-client/src/test_utils/mod.rs b/azalea-client/src/test_utils/mod.rs new file mode 100644 index 00000000..9e640908 --- /dev/null +++ b/azalea-client/src/test_utils/mod.rs @@ -0,0 +1,6 @@ +pub mod simulation; +pub mod tracing; + +pub mod prelude { + pub use super::{simulation::*, tracing::*}; +} diff --git a/azalea-client/src/test_utils/simulation.rs b/azalea-client/src/test_utils/simulation.rs new file mode 100644 index 00000000..c53a624a --- /dev/null +++ b/azalea-client/src/test_utils/simulation.rs @@ -0,0 +1,296 @@ +use std::{fmt::Debug, sync::Arc}; + +use azalea_auth::game_profile::GameProfile; +use azalea_block::BlockState; +use azalea_buf::AzaleaWrite; +use azalea_core::{ + delta::PositionDelta8, + game_type::{GameMode, OptionalGameType}, + position::{ChunkPos, Vec3}, + resource_location::ResourceLocation, + tick::GameTick, +}; +use azalea_entity::metadata::PlayerMetadataBundle; +use azalea_protocol::packets::{ + ConnectionProtocol, Packet, ProtocolPacket, + common::CommonPlayerSpawnInfo, + config::{ClientboundFinishConfiguration, ClientboundRegistryData}, + game::{ + ClientboundAddEntity, ClientboundLevelChunkWithLight, ClientboundLogin, ClientboundRespawn, + c_level_chunk_with_light::ClientboundLevelChunkPacketData, + c_light_update::ClientboundLightUpdatePacketData, + }, +}; +use azalea_registry::{Biome, DimensionType, EntityKind}; +use azalea_world::{Chunk, Instance, MinecraftEntityId, Section, palette::PalettedContainer}; +use bevy_app::App; +use bevy_ecs::{component::Mutable, prelude::*, schedule::ExecutorKind}; +use parking_lot::RwLock; +use simdnbt::owned::{NbtCompound, NbtTag}; +use uuid::Uuid; + +use crate::{ + ClientInformation, InConfigState, LocalPlayerBundle, connection::RawConnection, + disconnect::DisconnectEvent, local_player::InstanceHolder, player::GameProfileComponent, +}; + +/// A way to simulate a client in a server, used for some internal tests. +pub struct Simulation { + pub app: App, + pub entity: Entity, + + // the runtime needs to be kept around for the tasks to be considered alive + pub rt: tokio::runtime::Runtime, +} + +impl Simulation { + pub fn new(initial_connection_protocol: ConnectionProtocol) -> Self { + let mut app = create_simulation_app(); + let mut entity = app.world_mut().spawn_empty(); + let (player, rt) = + create_local_player_bundle(entity.id(), ConnectionProtocol::Configuration); + entity.insert(player); + + let entity = entity.id(); + + tick_app(&mut app); + + // start in the config state + app.world_mut().entity_mut(entity).insert(( + InConfigState, + GameProfileComponent(GameProfile::new( + Uuid::from_u128(1234), + "azalea".to_string(), + )), + )); + tick_app(&mut app); + + let mut simulation = Self { app, entity, rt }; + + #[allow(clippy::single_match)] + match initial_connection_protocol { + ConnectionProtocol::Configuration => {} + ConnectionProtocol::Game => { + simulation.receive_packet(ClientboundRegistryData { + registry_id: ResourceLocation::new("minecraft:dimension_type"), + entries: vec![( + ResourceLocation::new("minecraft:overworld"), + Some(NbtCompound::from_values(vec![ + ("height".into(), NbtTag::Int(384)), + ("min_y".into(), NbtTag::Int(-64)), + ])), + )] + .into_iter() + .collect(), + }); + + simulation.receive_packet(ClientboundFinishConfiguration); + simulation.tick(); + } + _ => unimplemented!("unsupported ConnectionProtocol {initial_connection_protocol:?}"), + } + + simulation + } + + pub fn receive_packet(&mut self, packet: impl Packet

) { + let buf = azalea_protocol::write::serialize_packet(&packet.into_variant()).unwrap(); + self.with_component_mut::(|raw_conn| { + raw_conn.injected_clientbound_packets.push(buf); + }); + } + + pub fn tick(&mut self) { + tick_app(&mut self.app); + } + + pub fn minecraft_entity_id(&self) -> MinecraftEntityId { + self.component::() + } + + pub fn component(&self) -> T { + self.app.world().get::(self.entity).unwrap().clone() + } + pub fn get_component(&self) -> Option { + self.app.world().get::(self.entity).cloned() + } + pub fn has_component(&self) -> bool { + self.app.world().get::(self.entity).is_some() + } + pub fn with_component_mut>( + &mut self, + f: impl FnOnce(&mut T), + ) { + f(&mut self + .app + .world_mut() + .entity_mut(self.entity) + .get_mut::() + .unwrap()); + } + pub fn resource(&self) -> T { + self.app.world().get_resource::().unwrap().clone() + } + pub fn with_resource(&self, f: impl FnOnce(&T)) { + f(self.app.world().get_resource::().unwrap()); + } + pub fn with_resource_mut(&mut self, f: impl FnOnce(Mut)) { + f(self.app.world_mut().get_resource_mut::().unwrap()); + } + + pub fn chunk(&self, chunk_pos: ChunkPos) -> Option>> { + self.component::() + .instance + .read() + .chunks + .get(&chunk_pos) + } + + pub fn disconnect(&mut self) { + // send DisconnectEvent + self.app.world_mut().send_event(DisconnectEvent { + entity: self.entity, + reason: None, + }); + } +} + +#[allow(clippy::type_complexity)] +fn create_local_player_bundle( + entity: Entity, + connection_protocol: ConnectionProtocol, +) -> (LocalPlayerBundle, tokio::runtime::Runtime) { + // unused since we'll trigger ticks ourselves + + let rt = tokio::runtime::Runtime::new().unwrap(); + + let raw_connection = RawConnection::new_networkless(connection_protocol); + + let instance = Instance::default(); + let instance_holder = InstanceHolder::new(entity, Arc::new(RwLock::new(instance))); + + let local_player_bundle = LocalPlayerBundle { + raw_connection, + client_information: ClientInformation::default(), + instance_holder, + metadata: PlayerMetadataBundle::default(), + }; + + (local_player_bundle, rt) +} + +fn create_simulation_app() -> App { + let mut app = App::new(); + + #[cfg(feature = "log")] + app.add_plugins( + bevy_app::PluginGroup::build(crate::DefaultPlugins).disable::(), + ); + + app.edit_schedule(bevy_app::Main, |schedule| { + // makes test results more reproducible + schedule.set_executor_kind(ExecutorKind::SingleThreaded); + }); + app +} + +fn tick_app(app: &mut App) { + app.update(); + app.world_mut().run_schedule(GameTick); +} + +pub fn make_basic_login_packet( + dimension_type: DimensionType, + dimension: ResourceLocation, +) -> ClientboundLogin { + ClientboundLogin { + player_id: MinecraftEntityId(0), + hardcore: false, + levels: vec![], + max_players: 20, + chunk_radius: 8, + simulation_distance: 8, + reduced_debug_info: false, + show_death_screen: true, + do_limited_crafting: false, + common: CommonPlayerSpawnInfo { + dimension_type, + dimension, + seed: 0, + game_type: GameMode::Survival, + previous_game_type: OptionalGameType(None), + is_debug: false, + is_flat: false, + last_death_location: None, + portal_cooldown: 0, + sea_level: 63, + }, + enforces_secure_chat: false, + } +} + +pub fn make_basic_respawn_packet( + dimension_type: DimensionType, + dimension: ResourceLocation, +) -> ClientboundRespawn { + ClientboundRespawn { + common: CommonPlayerSpawnInfo { + dimension_type, + dimension, + seed: 0, + game_type: GameMode::Survival, + previous_game_type: OptionalGameType(None), + is_debug: false, + is_flat: false, + last_death_location: None, + portal_cooldown: 0, + sea_level: 63, + }, + data_to_keep: 0, + } +} + +pub fn make_basic_empty_chunk( + pos: ChunkPos, + section_count: usize, +) -> ClientboundLevelChunkWithLight { + let mut chunk_bytes = Vec::new(); + let mut sections = Vec::new(); + for _ in 0..section_count { + sections.push(Section { + block_count: 0, + states: PalettedContainer::::new(), + biomes: PalettedContainer::::new(), + }); + } + sections.azalea_write(&mut chunk_bytes).unwrap(); + + ClientboundLevelChunkWithLight { + x: pos.x, + z: pos.z, + chunk_data: ClientboundLevelChunkPacketData { + heightmaps: Default::default(), + data: Arc::new(chunk_bytes.into()), + block_entities: vec![], + }, + light_data: ClientboundLightUpdatePacketData::default(), + } +} + +pub fn make_basic_add_entity( + entity_type: EntityKind, + id: i32, + position: impl Into, +) -> ClientboundAddEntity { + ClientboundAddEntity { + id: id.into(), + uuid: Uuid::from_u128(1234), + entity_type, + position: position.into(), + x_rot: 0, + y_rot: 0, + y_head_rot: 0, + data: 0, + velocity: PositionDelta8::default(), + } +} diff --git a/azalea-client/src/test_utils/tracing.rs b/azalea-client/src/test_utils/tracing.rs new file mode 100644 index 00000000..85ac4bd6 --- /dev/null +++ b/azalea-client/src/test_utils/tracing.rs @@ -0,0 +1,38 @@ +use bevy_log::tracing_subscriber::{ + self, EnvFilter, Layer, + layer::{Context, SubscriberExt}, + registry::LookupSpan, + util::SubscriberInitExt, +}; +use tracing::{Event, Level, Subscriber, level_filters::LevelFilter}; + +pub fn init_tracing() { + init_tracing_with_level(Level::WARN); +} + +pub fn init_tracing_with_level(max_level: Level) { + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer().with_filter( + EnvFilter::builder() + .with_default_directive(max_level.into()) + .from_env_lossy(), + ), + ) + .with(TestTracingLayer { + panic_on_level: max_level, + }) + .init(); +} + +struct TestTracingLayer { + panic_on_level: Level, +} +impl Layer for TestTracingLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let level = *event.metadata().level(); + if level <= self.panic_on_level { + panic!("logged on level {level}"); + } + } +} -- cgit v1.2.3