~hime/protochat

e5f668374c930e1c1a07ac6215cdf241c08f3d32 — drbawb 11 months ago 86db955
(net): add tests for client state machine

- brings in `sluice` crate to create an in-memory bidirectional
  stream which we can use to model a fake TCP connection.

- adds tests surrounding the initial client/server registration
  handshake.
3 files changed, 162 insertions(+), 7 deletions(-)

M Cargo.lock
M linetest/Cargo.toml
M linetest/src/shell/net.rs
M Cargo.lock => Cargo.lock +13 -0
@@ 309,6 309,7 @@ dependencies = [
 "byteorder",
 "crossterm",
 "futures-util",
 "sluice",
 "smol",
 "smolboi",
 "textwrap",


@@ 546,6 547,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"

[[package]]
name = "sluice"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed13b7cb46f13a15db2c4740f087a848acc8b31af89f95844d40137451f89b1"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-io",
 "futures-util",
]

[[package]]
name = "smallvec"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"

M linetest/Cargo.toml => linetest/Cargo.toml +2 -1
@@ 15,6 15,7 @@ async-mutex = "1.0"
byteorder = "1.0"
crossterm = "0.17"
futures-util = "0.3"
sluice = "0.5"
smol = "0.1"
textwrap = "0.12"
thiserror = "1.0"


@@ 24,4 25,4 @@ void = "1.0"

[dependencies.smolboi]
version = "0.1.0"
path = "../smolboi"
\ No newline at end of file
path = "../smolboi"

M linetest/src/shell/net.rs => linetest/src/shell/net.rs +147 -6
@@ 18,23 18,26 @@ enum ClientError {
    InvalidState,
}

#[derive(Debug, Eq, PartialEq)]
enum ClientState {
    Connecting,
    Connected,
}

struct Client {
#[derive(Debug)]
struct Client<S> {
    conn: Connection,
    outbox: Sender<ConnMsg>,
    sock: Arc<Async<TcpStream>>,
    sock: S,
    state: ClientState,
}

impl Client {
    pub fn with(conn: Connection, sock: &Arc<Async<TcpStream>>, outbox: Sender<ConnMsg>) -> Self {
impl<S> Client<S> 
where S: AsyncReadExt + AsyncWriteExt + Unpin {
    pub fn with(conn: Connection, sock: S, outbox: Sender<ConnMsg>) -> Self {
        Self {
            conn: conn,
            sock: sock.clone(),
            sock: sock,
            outbox: outbox,
            state: ClientState::Connecting,
        }


@@ 241,7 244,7 @@ async fn net_worker_task(
    let mut task_rx = shell_mx.inbox.fuse();
    let mut quit_rx = quit_rx.fuse();

    let mut client = Client::with(conn, &sock, shell_mx.outbox.clone());
    let mut client = Client::with(conn, sock.clone(), shell_mx.outbox.clone());

    loop {
        select! {


@@ 275,3 278,141 @@ async fn net_worker_task(
    Ok(shell_mx.outbox.send(log_line).await?)
}

#[cfg(test)]
mod tests {
    use async_channel::unbounded;
    use core::pin::Pin;
    use futures_util::io::{self, AsyncRead, AsyncWrite, IoSlice, IoSliceMut};
    use futures_util::task::{Context, Poll};
    use sluice::pipe::{pipe, PipeReader, PipeWriter};
    use super::*;


    struct FakeTcpStream {
        rx: PipeReader,
        tx: PipeWriter,
    }

    impl FakeTcpStream {
        pub fn new() -> (Self, Self) {
            let (rx_a, tx_a) = pipe();
            let (rx_b, tx_b) = pipe();

            // create a "crossover" cable
            let socket_a = Self { rx: rx_b, tx: tx_a };
            let socket_b = Self { rx: rx_a, tx: tx_b };

            (socket_a, socket_b)
        }
    }

    impl AsyncRead for FakeTcpStream {
        fn poll_read(
            mut self: Pin<&mut Self>,
            cx: &mut Context<'_>,
            buf: &mut [u8]
        ) -> Poll<io::Result<usize>> {
            PipeReader::poll_read(Pin::new(&mut self.rx), cx, buf)
        }

        fn poll_read_vectored(
            mut self: Pin<&mut Self>,
            cx: &mut Context<'_>,
            bufs: &mut [IoSliceMut<'_>]
        ) -> Poll<io::Result<usize>> {
            PipeReader::poll_read_vectored(Pin::new(&mut self.rx), cx, bufs)
        }
    }

    impl AsyncWrite for FakeTcpStream {
        fn poll_write(
            mut self: Pin<&mut Self>,
            cx: &mut Context<'_>,
            buf: &[u8]
        ) -> Poll<io::Result<usize>> {
            PipeWriter::poll_write(Pin::new(&mut self.tx), cx, buf)
        }

        fn poll_write_vectored(
            mut self: Pin<&mut Self>,
            cx: &mut Context<'_>,
            bufs: &[IoSlice<'_>]
        ) -> Poll<io::Result<usize>> {
            PipeWriter::poll_write_vectored(Pin::new(&mut self.tx), cx, bufs)
        }

        fn poll_flush(
            mut self: Pin<&mut Self>,
            cx: &mut Context<'_>
        ) -> Poll<io::Result<()>> {
            PipeWriter::poll_flush(Pin::new(&mut self.tx), cx)
        }

        fn poll_close(mut self: Pin<&mut Self>,
            cx: &mut Context<'_>
        ) -> Poll<io::Result<()>> {
            PipeWriter::poll_close(Pin::new(&mut self.tx), cx)
        }
    }


    const CLIENT_NAME: &'static str = "Chuck Testa";
    const CLIENT_ADDR: &'static str = "127.0.0.1:1234";

    fn init_fake_connection() -> Connection {
        Connection {
            id: 0,
            name: CLIENT_NAME.to_string(),
            addr: CLIENT_ADDR.parse().unwrap(),
            is_connected: false,
        }
    }

    #[test]
    fn client_accepted_registration() -> anyhow::Result<()> {
        // initial setup 
        let (client_sock, _server_sock) = FakeTcpStream::new();
        let (mx_tx, _mx_rx) = unbounded();

        // an initialized client should not be considered connected ...
        let mut client = Client::with(init_fake_connection(), client_sock, mx_tx);
        assert_eq!(client.state, ClientState::Connecting);

        // an accepted registration should result in a state change
        let resp = Packet::RegistrationAccepted { 
            name: "Chuck Testa".to_string()
        };

        smol::block_on(client.handle_net_pkt(resp))?;
        assert_eq!(client.state, ClientState::Connected);

        Ok(())
    }

    #[test]
    fn client_rejected_registration() -> anyhow::Result<()> {
        // initial setup 
        let (client_sock, mut server_sock) = FakeTcpStream::new();
        let (mx_tx, _mx_rx) = unbounded();
        let mut client = Client::with(init_fake_connection(), client_sock, mx_tx);

        // rejected client should still be in Connecting state
        smol::block_on(client.handle_net_pkt(Packet::RegistrationRejected))?;
        assert_eq!(client.state, ClientState::Connecting);

        // tell client to change nick
        let new_name = "newer_more_different_name";
        smol::block_on(client.handle_shell_msg(ConnMsg::ChangeNick {
            name: new_name.to_string()
        }))?;

        // the "server" should have received a registration packet for the new name
        let req = smol::block_on(smolboi::util::read_packet(&mut server_sock.rx))?;
        if let Packet::Register { name } = req {
            assert_eq!(name, new_name);
        }

        Ok(())
    }

}