Skip to main content

chunkedge/
testing.rs

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