chunkedge/
testing.rs

1use std::collections::VecDeque;
2use std::net::{IpAddr, Ipv4Addr};
3use std::sync::{Arc, Mutex};
4use std::time::{Duration, Instant};
5
6use bevy_app::prelude::*;
7use bevy_ecs::prelude::*;
8use bytes::{Buf, BufMut, BytesMut};
9use chunkedge_binary::{Decode, Encode};
10use chunkedge_ident::ident;
11use chunkedge_network::NetworkPlugin;
12use chunkedge_registry::{BiomeRegistry, DimensionTypeRegistry};
13use chunkedge_server::client::{ClientBundle, ClientBundleArgs, ClientConnection, ReceivedPacket};
14use chunkedge_server::keepalive::KeepaliveSettings;
15use chunkedge_server::protocol::decode::PacketFrame;
16use chunkedge_server::protocol::packets::play::{AcceptTeleportationC2s, PlayerPositionS2c};
17use chunkedge_server::protocol::{Packet, PacketDecoder, PacketEncoder, VarInt};
18use chunkedge_server::{ChunkLayer, EntityLayer, Server, ServerSettings};
19use uuid::Uuid;
20
21use crate::DefaultPlugins;
22pub struct ScenarioSingleClient {
23    /// The new bevy application.
24    pub app: App,
25    /// Entity handle for the single client.
26    pub client: Entity,
27    /// Helper for sending and receiving packets from the mock client.
28    pub helper: MockClientHelper,
29    /// Entity with [`ChunkLayer`] and [`EntityLayer`] components.
30    pub layer: Entity,
31}
32
33impl ScenarioSingleClient {
34    /// Sets up ChunkEdge with a single mock client and entity+chunk layer. The
35    /// client is configured to be placed within the layer.
36    ///
37    /// Reduces boilerplate in unit tests.
38    pub fn new() -> Self {
39        let mut app = App::new();
40
41        app.insert_resource(KeepaliveSettings {
42            period: Duration::MAX,
43        })
44        .insert_resource(ServerSettings {
45            compression_threshold: Default::default(),
46            ..Default::default()
47        })
48        .add_plugins(DefaultPlugins.build().disable::<NetworkPlugin>());
49
50        app.update(); // Initialize plugins.
51
52        let chunk_layer = ChunkLayer::new(
53            ident!("overworld"),
54            app.world().resource::<DimensionTypeRegistry>(),
55            app.world().resource::<BiomeRegistry>(),
56            app.world().resource::<Server>(),
57        );
58        let entity_layer = EntityLayer::new(app.world().resource::<Server>());
59        let layer = app.world_mut().spawn((chunk_layer, entity_layer)).id();
60
61        let (mut client, helper) = create_mock_client("test");
62        client.player.layer.0 = layer;
63        client.visible_chunk_layer.0 = layer;
64        client.visible_entity_layers.0.insert(layer);
65        let client = app.world_mut().spawn(client).id();
66
67        ScenarioSingleClient {
68            app,
69            client,
70            helper,
71            layer,
72        }
73    }
74}
75
76impl Default for ScenarioSingleClient {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82/// Creates a mock client bundle that can be used for unit testing.
83///
84/// Returns the client, and a helper to inject packets as if the client sent
85/// them and receive packets as if the client received them.
86pub fn create_mock_client<N: Into<String>>(name: N) -> (ClientBundle, MockClientHelper) {
87    let conn = MockClientConnection::new();
88
89    let bundle = ClientBundle::new(ClientBundleArgs {
90        username: name.into(),
91        uuid: Uuid::from_bytes(rand::random()),
92        ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
93        properties: Default::default(),
94        conn: Box::new(conn.clone()),
95        view_distance: 2,
96        locale: Default::default(),
97        chat_mode: Default::default(),
98        chat_colors: false,
99        displayed_skin_parts: Default::default(),
100        main_arm: Default::default(),
101        enable_text_filtering: false,
102        allow_server_listings: false,
103        particle_mode: Default::default(),
104        enc: PacketEncoder::new(),
105    });
106
107    let helper = MockClientHelper::new(conn);
108
109    (bundle, helper)
110}
111
112/// A mock client connection that can be used for testing.
113///
114/// Safe to clone, but note that the clone will share the same buffers.
115#[derive(Clone)]
116pub struct MockClientConnection {
117    inner: Arc<Mutex<MockClientConnectionInner>>,
118}
119
120struct MockClientConnectionInner {
121    /// The queue of packets to receive from the client to be processed by the
122    /// server.
123    recv_buf: VecDeque<ReceivedPacket>,
124    /// The queue of packets to send from the server to the client.
125    send_buf: BytesMut,
126}
127
128impl MockClientConnection {
129    pub fn new() -> Self {
130        Self {
131            inner: Arc::new(Mutex::new(MockClientConnectionInner {
132                recv_buf: VecDeque::new(),
133                send_buf: BytesMut::new(),
134            })),
135        }
136    }
137
138    /// Injects a (Packet ID + data) frame to be received by the server.
139    fn inject_send(&self, mut bytes: BytesMut) {
140        let id = VarInt::decode_partial((&mut bytes).reader()).expect("failed to decode packet ID");
141
142        self.inner
143            .lock()
144            .unwrap()
145            .recv_buf
146            .push_back(ReceivedPacket {
147                timestamp: Instant::now(),
148                id,
149                body: bytes.freeze(),
150            });
151    }
152
153    fn take_received(&self) -> BytesMut {
154        self.inner.lock().unwrap().send_buf.split()
155    }
156
157    fn clear_received(&self) {
158        self.inner.lock().unwrap().send_buf.clear();
159    }
160}
161
162impl ClientConnection for MockClientConnection {
163    fn try_send(&mut self, bytes: BytesMut) -> anyhow::Result<()> {
164        self.inner.lock().unwrap().send_buf.unsplit(bytes);
165        Ok(())
166    }
167
168    fn try_recv(&mut self) -> anyhow::Result<Option<ReceivedPacket>> {
169        Ok(self.inner.lock().unwrap().recv_buf.pop_front())
170    }
171
172    fn len(&self) -> usize {
173        self.inner.lock().unwrap().recv_buf.len()
174    }
175}
176
177impl Default for MockClientConnection {
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183/// Contains the mocked client connection and helper methods to inject packets
184/// and read packets from the send stream.
185pub struct MockClientHelper {
186    conn: MockClientConnection,
187    dec: PacketDecoder,
188    scratch: BytesMut,
189}
190
191impl MockClientHelper {
192    pub fn new(conn: MockClientConnection) -> Self {
193        Self {
194            conn,
195            dec: PacketDecoder::new(),
196            scratch: BytesMut::new(),
197        }
198    }
199
200    /// Inject a packet to be treated as a packet inbound to the server. Panics
201    /// if the packet cannot be sent.
202    #[track_caller]
203    pub fn send<P>(&mut self, packet: &P)
204    where
205        P: Packet + Encode,
206    {
207        packet
208            .encode_with_id((&mut self.scratch).writer())
209            .expect("failed to encode packet");
210
211        self.conn.inject_send(self.scratch.split());
212    }
213
214    /// Collect all packets that have been received by the client.
215    #[track_caller]
216    pub fn collect_received(&mut self) -> PacketFrames {
217        self.dec.queue_bytes(self.conn.take_received());
218
219        let mut res = vec![];
220
221        while let Some(frame) = self
222            .dec
223            .try_next_packet()
224            .expect("failed to decode packet frame")
225        {
226            res.push(frame);
227        }
228
229        PacketFrames(res)
230    }
231
232    pub fn clear_received(&mut self) {
233        self.conn.clear_received();
234    }
235
236    pub fn confirm_initial_pending_teleports(&mut self) {
237        let mut counter = 0;
238
239        for pkt in self.collect_received().0 {
240            if pkt.id == PlayerPositionS2c::ID {
241                pkt.decode::<PlayerPositionS2c>().unwrap();
242
243                self.send(&AcceptTeleportationC2s {
244                    teleport_id: counter.into(),
245                });
246
247                counter += 1;
248            }
249        }
250    }
251}
252
253#[derive(Clone, Debug)]
254pub struct PacketFrames(pub Vec<PacketFrame>);
255
256impl PacketFrames {
257    #[track_caller]
258    pub fn assert_count<P: Packet>(&self, expected_count: usize) {
259        let actual_count = self.0.iter().filter(|f| f.id == P::ID).count();
260
261        assert_eq!(
262            expected_count,
263            actual_count,
264            "unexpected packet count for {} (expected {expected_count}, got {actual_count})",
265            P::NAME,
266        )
267    }
268
269    #[track_caller]
270    pub fn assert_order<L: PacketList>(&self) {
271        let positions: Vec<_> = self
272            .0
273            .iter()
274            .filter_map(|f| L::packets().iter().position(|(id, _)| f.id == *id))
275            .collect();
276
277        // TODO: replace with slice::is_sorted when stabilized.
278        let is_sorted = positions.windows(2).all(|w| w[0] <= w[1]);
279
280        assert!(
281            is_sorted,
282            "packets out of order (expected {:?}, got {:?})",
283            L::packets(),
284            self.debug_order::<L>()
285        )
286    }
287
288    /// Finds the first occurrence of `P` in the packet list and decodes it.
289    ///
290    /// # Panics
291    ///
292    /// Panics if the packet was not found or a decoding error occurs.
293    #[track_caller]
294    pub fn first<'a, P>(&'a self) -> P
295    where
296        P: Packet + Decode<'a>,
297    {
298        if let Some(frame) = self.0.iter().find(|p| p.id == P::ID) {
299            frame.decode::<P>().unwrap()
300        } else {
301            panic!("failed to find packet {}", P::NAME)
302        }
303    }
304
305    pub fn debug_order<L: PacketList>(&self) -> impl std::fmt::Debug {
306        self.0
307            .iter()
308            .filter_map(|f| L::packets().iter().find(|(id, _)| f.id == *id).copied())
309            .collect::<Vec<_>>()
310    }
311}
312
313pub trait PacketList {
314    fn packets() -> &'static [(i32, &'static str)];
315}
316
317macro_rules! impl_packet_list {
318    ($($ty:ident),*) => {
319        impl<$($ty: Packet,)*> PacketList for ($($ty,)*) {
320            fn packets() -> &'static [(i32, &'static str)] {
321                &[
322                    $(
323                        (
324                            $ty::ID,
325                            $ty::NAME
326                        ),
327                    )*
328                ]
329            }
330        }
331    }
332}
333
334impl_packet_list!(A);
335impl_packet_list!(A, B);
336impl_packet_list!(A, B, C);
337impl_packet_list!(A, B, C, D);
338impl_packet_list!(A, B, C, D, E);
339impl_packet_list!(A, B, C, D, E, F);
340impl_packet_list!(A, B, C, D, E, F, G);
341impl_packet_list!(A, B, C, D, E, F, G, H);
342impl_packet_list!(A, B, C, D, E, F, G, H, I);
343impl_packet_list!(A, B, C, D, E, F, G, H, I, J);
344impl_packet_list!(A, B, C, D, E, F, G, H, I, J, K);