Skip to content
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

[ECS] Infra for comprehensive unit tests #198

Merged
merged 47 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
79de8a2
add a very minimal integration test that does not involve any clients
dyc3 Jan 26, 2023
4f425ec
add an example test where we send a packet and assert
dyc3 Jan 26, 2023
d5decfa
add MockClientHelper to reduce repetition
dyc3 Jan 26, 2023
58a5931
set up valence_network
dyc3 Jan 27, 2023
19657e7
add dependencies for valence_network
dyc3 Jan 27, 2023
aa3dc85
move packet module to valence_network
dyc3 Jan 27, 2023
8429735
add config module to valence_network
dyc3 Jan 27, 2023
b1014d8
move server stuff to valence_network, excluding stuff about dimension…
dyc3 Jan 27, 2023
b43f794
revert valence_network stuff because brain hurt
dyc3 Jan 28, 2023
4822d37
add PacketStream trait and start implementing MockPacketStream
dyc3 Jan 28, 2023
143d746
make PacketStream pub
dyc3 Jan 29, 2023
ee4ed40
fix borrow errors in flush_sent
dyc3 Jan 29, 2023
5b19a6d
ignore dead code lint for MockPacketStream::new
dyc3 Jan 29, 2023
45de2ad
impl RealPacketStream
dyc3 Jan 29, 2023
0f7b2a7
rework everything into new PacketStreamer type
dyc3 Jan 29, 2023
a41ef96
gut most functionality out of PlayPacketSender/Receiver
dyc3 Jan 29, 2023
de1b269
fix some lifetime stuff
dyc3 Jan 30, 2023
3678e11
small refactor
dyc3 Jan 30, 2023
03a3d3d
implement async task cleanup when client gets destroyed
dyc3 Jan 30, 2023
1f5fac7
remove unused imports
dyc3 Jan 30, 2023
d7c891f
change MockPacketStream to use BytesMut instead of Vec<u8> for buffer…
dyc3 Jan 30, 2023
5185005
Implement PacketDecoder::collect_into_vec
rj00a Feb 1, 2023
5b5c03c
reimpl collect_sent using PacketDecoder::collect_into_vec
dyc3 Feb 1, 2023
053a14f
remove async from example unit test
dyc3 Feb 1, 2023
03b2f9e
queue flushed bytes
dyc3 Feb 1, 2023
4092b0e
fix test_mock_stream_assert_sent
dyc3 Feb 1, 2023
4fe9173
fix infinite loop in try_recv
dyc3 Feb 1, 2023
ad35087
change an unwrap to expect
dyc3 Feb 1, 2023
327abd9
fix impl of MockClientHelper::send_packet
dyc3 Feb 1, 2023
0dad7a3
add `example_test_open_inventory`
dyc3 Feb 1, 2023
ed1874e
Merge branch 'ecs_rewrite' into ecs-unit-tests
dyc3 Feb 2, 2023
b6411c9
remove packet_stream module
dyc3 Feb 2, 2023
b99dad2
add `MockClientConnection`
dyc3 Feb 2, 2023
aac98df
remove a bunch of old shit and comment some stuff out to make it compile
dyc3 Feb 2, 2023
b002f96
make MockClientConnection cloneable so we can have multiple references.
dyc3 Feb 2, 2023
199ab18
reimpl MockClientHelper so it uses MockClientConnection
dyc3 Feb 2, 2023
9d61e8c
fix example unit tests
dyc3 Feb 2, 2023
99ec49c
adjust module visibility
dyc3 Feb 2, 2023
5aadc5e
move MockClientConnection into unit test utils module
dyc3 Feb 2, 2023
bc8b91c
eliminate some lint warnings
dyc3 Feb 2, 2023
56b6a85
add `scenario_single_client` testing utility to reduce boilerplate
dyc3 Feb 2, 2023
0970df0
add macros to make asserting packet count and order less verbose
dyc3 Feb 2, 2023
f075bf1
add unit test cookbook
dyc3 Feb 2, 2023
996878e
enable doctests to discover the docs in unit_tests::example
dyc3 Feb 2, 2023
dcbb9fb
remove unnecessary drop
dyc3 Feb 3, 2023
e069fd5
fix nits
dyc3 Feb 3, 2023
2501c11
improve assert fail message for `assert_packet_count!`
dyc3 Feb 3, 2023
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 crates/valence_new/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ pub mod instance;
pub mod inventory;
pub mod math;
mod packet;
pub(crate) mod packet_stream;
pub mod player_list;
pub mod player_textures;
pub mod server;
#[cfg(test)]
mod unit_test;

/// A [`Component`] for marking entities that should be despawned at the end of
/// the tick.
Expand Down
181 changes: 181 additions & 0 deletions crates/valence_new/src/packet_stream.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use std::collections::VecDeque;

use bytes::{BufMut, BytesMut};
use valence_protocol::packets::S2cPlayPacket;
use valence_protocol::{DecodePacket, EncodePacket, PacketDecoder, PacketEncoder};

use crate::server::byte_channel::{ByteReceiver, ByteSender, TryRecvError};

/// Represents a byte stream that packets can be read from and written to.
pub trait PacketStream {
/// Parses and returns the next packet in the stream.
fn try_recv<'a, P>(&'a mut self) -> anyhow::Result<Option<P>>
where
P: DecodePacket<'a> + std::fmt::Debug;

/// Encodes and writes the given packet to the stream.
fn try_send<P>(&mut self, packet: P) -> anyhow::Result<()>
where
P: EncodePacket;
}

pub(crate) struct RealPacketStream {
dec: PacketDecoder,
recv: ByteReceiver,
enc: PacketEncoder,
send: ByteSender,
}

impl RealPacketStream {
pub(crate) fn new(recv: ByteReceiver, send: ByteSender) -> Self {
Self {
dec: PacketDecoder::new(),
recv,
enc: PacketEncoder::new(),
send,
}
}

pub fn flush(&mut self) -> anyhow::Result<()> {
let bytes = self.enc.take();
self.send.try_send(bytes)?;
Ok(())
}
}

impl PacketStream for RealPacketStream {
fn try_recv<'a, P>(&'a mut self) -> anyhow::Result<Option<P>>
where
P: DecodePacket<'a> + std::fmt::Debug,
{
loop {
match self.recv.try_recv() {
Ok(bytes) => self.dec.queue_bytes(bytes),
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => anyhow::bail!("Disconnected"),
}
}

self.dec.try_next_packet()
}

fn try_send<P>(&mut self, packet: P) -> anyhow::Result<()>
where
P: EncodePacket,
{
self.enc.append_packet(&packet)?;
Ok(())
}
}

/// A `PacketStream` that reads and writes from an in memory buffer of packets
/// used for testing.
pub(crate) struct MockPacketStream {
recv_enc: PacketEncoder,
recv_dec: PacketDecoder,

send_queue: VecDeque<Vec<u8>>,
}

impl<'a> MockPacketStream {
#[allow(dead_code)]
pub(crate) fn new() -> Self {
Self {
recv_enc: PacketEncoder::new(),
recv_dec: PacketDecoder::new(),
send_queue: VecDeque::new(),
}
}

/// Injects a packet into the receive stream as if it were received from a
/// client.
///
/// ```rust
/// use valence_new::packet_stream::MockPacketStream;
/// use valence_protocol::packets::c2s::play::KeepAliveC2s;
///
/// let mut stream = MockPacketStream::new();
/// let packet = KeepAliveC2s { id: 0xdeadbeef };
/// stream.inject_recv(packet);
/// ```
#[allow(dead_code)]
pub(crate) fn inject_recv<P>(&mut self, packet: P)
where
P: EncodePacket,
{
self.recv_enc
.append_packet(&packet)
.expect("failed to encode injected packet");
let bytes = self.recv_enc.take();
self.recv_dec.queue_bytes(bytes);
}

fn queue_send<P>(&mut self, packet: P) -> anyhow::Result<()>
where
P: EncodePacket,
{
let bytes = BytesMut::new();
let mut w = bytes.writer();
P::encode_packet(&packet, &mut w)?;
let bytes = w.into_inner();
self.send_queue.push_back(bytes.to_vec());
Ok(())
}

#[allow(dead_code)]
pub(crate) fn flush_sent(&'a mut self) -> anyhow::Result<Vec<S2cPlayPacket<'a>>> {
let mut packets = Vec::new();
for pkt in self.send_queue.iter() {
let mut p: Box<&'a [u8]> = Box::new(pkt.as_slice());
let packet = S2cPlayPacket::<'a>::decode_packet(&mut p)?;
packets.push(packet);
}
Ok(packets)
}
}

impl PacketStream for MockPacketStream {
fn try_recv<'a, P>(&'a mut self) -> anyhow::Result<Option<P>>
where
P: DecodePacket<'a> + std::fmt::Debug,
{
self.recv_dec.try_next_packet()
}

fn try_send<P>(&mut self, packet: P) -> anyhow::Result<()>
where
P: EncodePacket,
{
self.queue_send(packet)
}
}

#[cfg(test)]
mod tests {
use valence_protocol::packets::c2s::play::KeepAliveC2s;
use valence_protocol::packets::s2c::play::KeepAliveS2c;

use super::*;

#[test]
fn test_mock_stream_read() {
let mut stream = MockPacketStream::new();
let packet = KeepAliveC2s { id: 0xdeadbeef };
stream.inject_recv(packet.clone());
let packet_out = stream.try_recv::<KeepAliveC2s>().unwrap().unwrap();
assert_eq!(packet.id, packet_out.id);
}

#[test]
fn test_mock_stream_assert_sent() {
let mut stream = MockPacketStream::new();
let packet = KeepAliveS2c { id: 0xdeadbeef };
stream.try_send(packet.clone()).unwrap();
let packets_out = stream.flush_sent().unwrap();
let S2cPlayPacket::KeepAliveS2c(packet_out) = packets_out[0] else {
assert!(false);
return;
};
assert_eq!(packet.id, packet_out.id);
}
}
8 changes: 7 additions & 1 deletion crates/valence_new/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use crate::player_list::{update_player_list, PlayerList};
use crate::server::connect::do_accept_loop;
use crate::Despawned;

mod byte_channel;
pub(crate) mod byte_channel;
mod connect;
mod packet_manager;

Expand Down Expand Up @@ -221,6 +221,12 @@ impl SharedServer {
self.0.connection_sema.close();
*self.0.shutdown_result.lock().unwrap() = Some(res.map_err(|e| e.into()));
}

/// Forcefully aquires a permit to connect to the server. This is useful
/// for testing.
pub(crate) fn force_aquire_owned(&self) -> OwnedSemaphorePermit {
self.0.connection_sema.clone().try_acquire_owned().unwrap()
}
}

/// Contains information about a new client joining the server.
Expand Down
29 changes: 29 additions & 0 deletions crates/valence_new/src/server/packet_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,21 @@ pub struct PlayPacketSender {
}

impl PlayPacketSender {
/// Creates a new `PlayPacketSender` that will send packets such that they
/// can be received and asserted on by a unit test.
pub(crate) fn new_injectable() -> (Self, ByteReceiver) {
let (send, recv) = byte_channel(0);
(
PlayPacketSender {
enc: PacketEncoder::new(),
send,
writer_task: None,
handle: Handle::current(),
},
recv,
)
}

pub fn append_packet<P>(&mut self, pkt: &P) -> Result<()>
where
P: EncodePacket + ?Sized,
Expand Down Expand Up @@ -262,6 +277,20 @@ pub struct PlayPacketReceiver {
}

impl PlayPacketReceiver {
/// Creates a new `PlayPacketReceiver` with a new `ByteSender` to manually
/// inject packets. This is useful for testing.
pub(crate) fn new_injectable() -> (Self, ByteSender) {
let (send, recv) = byte_channel(8192);
(
Self {
dec: Default::default(),
recv,
reader_task: tokio::spawn(async {}),
},
send,
)
}

pub fn try_next_packet<'a, P>(&'a mut self) -> Result<Option<P>>
where
P: DecodePacket<'a> + fmt::Debug,
Expand Down
61 changes: 61 additions & 0 deletions crates/valence_new/src/unit_test/example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use bevy_app::App;
use bevy_ecs::prelude::*;

use super::util::create_mock_client;
use crate::config::ServerPlugin;
use crate::server::Server;
use crate::unit_test::util::gen_client_info;

/// Examples of valence unit tests that need to test the behavior of the server,
/// and not just the logic of a single function. This module is meant to be a
/// pallette of examples for how to write such tests, with various levels of
/// complexity.
///
/// Some of the tests in this file may be inferior duplicates of real tests.
#[cfg(test)]
mod tests {
use bytes::BytesMut;

use super::*;
use crate::client::Client;

/// The server's tick should increment every update.
#[test]
fn test_server_tick_increment() {
let mut app = App::new();
app.add_plugin(ServerPlugin::new(()));
let server = app.world.resource::<Server>();
let tick = server.current_tick();
drop(server);
app.update();
let server = app.world.resource::<Server>();
assert_eq!(server.current_tick(), tick + 1);
}

/// A unit test where we want to test what happens when a client sends a
/// packet to the server.
#[tokio::test]
async fn test_client_position() {
let mut app = App::new();
app.add_plugin(ServerPlugin::new(()));
let server = app.world.resource::<Server>();
let permit = server.force_aquire_owned();
let info = gen_client_info("test");
let (client, mut client_helper) = create_mock_client(permit, info);
let client_ent = app.world.spawn(client).id();

// Send a packet as the client to the server.
let packet = valence_protocol::packets::c2s::play::SetPlayerPosition {
position: [12.0, 64.0, 0.0],
on_ground: true,
};
client_helper.send_packet(packet);

// Process the packet.
app.update();

// Make assertions
let client: &Client = app.world.get(client_ent).unwrap();
assert_eq!(client.position(), [12.0, 64.0, 0.0].into());
}
}
2 changes: 2 additions & 0 deletions crates/valence_new/src/unit_test/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod example;
pub(crate) mod util;
59 changes: 59 additions & 0 deletions crates/valence_new/src/unit_test/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use bytes::BytesMut;
use tokio::sync::OwnedSemaphorePermit;
use valence_protocol::{EncodePacket, Username};

use crate::client::Client;
use crate::server::byte_channel::{ByteReceiver, ByteSender};
use crate::server::{NewClientInfo, PlayPacketReceiver, PlayPacketSender};

/// Creates a mock client that can be used for unit testing.
///
/// Returns the client, and a helper to inject packets as if the client sent
/// them and receive packets as if the client received them.
pub fn create_mock_client(
permit: OwnedSemaphorePermit,
client_info: NewClientInfo,
) -> (Client, MockClientHelper) {
let (pkt_send, recv) = PlayPacketSender::new_injectable();
let (pkt_recv, send) = PlayPacketReceiver::new_injectable();

let client = Client::new(pkt_send, pkt_recv, permit, client_info);
(client, MockClientHelper::new(send, recv))
}

/// Creates a `NewClientInfo` with the given username and a random UUID.
/// Panics if the username is invalid.
pub fn gen_client_info(username: &str) -> NewClientInfo {
NewClientInfo {
username: Username::new(username.to_owned()).unwrap(),
uuid: uuid::Uuid::new_v4(),
ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
properties: vec![],
}
}

pub struct MockClientHelper {
/// Used to pretend the client sent bytes.
send: ByteSender,
/// Used to pretend the client received bytes.
recv: ByteReceiver,
}

/// Has a `ByteSender` to inject packets to be read and acted
/// upon by the server, and a `ByteReceiver` to receive packets and make
/// assertions about what the server sent.
impl MockClientHelper {
fn new(send: ByteSender, recv: ByteReceiver) -> Self {
Self { send, recv }
}

/// Inject a packet to be parsed by the server. Panics if the packet cannot
/// be sent.
pub fn send_packet(&mut self, packet: impl EncodePacket) {
let mut buffer = Vec::<u8>::new();
valence_protocol::encode_packet(&mut buffer, &packet).expect("Failed to encode packet");
self.send
.try_send(BytesMut::from(buffer.as_slice()))
.expect("Failed to send packet");
}
}