tonic-server-mock 1.0.1

Mock tonic-based gRPC servers that run in memory without networking
Documentation
use std::{convert::Infallible, sync::Arc};
use tokio::{io::DuplexStream, sync::mpsc::Sender};
use tonic::{
    body::BoxBody,
    codegen::{
        http::{Request, Response},
        Service,
    },
    server::NamedService,
    transport::{Channel, Error as TransportError},
};

pub trait Svc:
    Service<Request<BoxBody>, Response = Response<BoxBody>, Error = Infallible>
    + NamedService
    + Clone
    + Send
    + 'static
where
    Self::Future: Send + 'static,
{
}

impl<S> Svc for S
where
    S: Service<Request<BoxBody>, Response = Response<BoxBody>, Error = Infallible>
        + NamedService
        + Clone
        + Send
        + 'static,
    S::Future: Send + 'static,
{
}

pub type ConnectionMockSender = Sender<Result<DuplexStream, TransportError>>;

#[derive(Debug, Clone)]
pub struct EndpointMock {
    connection_sender: Arc<ConnectionMockSender>,
}

impl EndpointMock {
    pub fn new(connection_sender: ConnectionMockSender) -> Self {
        Self {
            connection_sender: Arc::new(connection_sender),
        }
    }

    pub async fn once(self) -> Channel {
        self.connect().await
    }

    pub async fn connect(&self) -> Channel {
        let connection_sender = Arc::clone(&self.connection_sender);

        let client_connector =
            ::tower::service_fn(move |/* mut */ uri: ::tonic::transport::Uri| {
                tracing::info!("connection to {:?}", uri);
                let connection_sender = Arc::clone(&connection_sender);
                async move {
                    let (client_io, server_io) = ::tokio::io::duplex(1024);
                    connection_sender.send(Ok(server_io)).await.unwrap();
                    Ok::<_, ::tonic::transport::Error>(::hyper_util::rt::TokioIo::new(client_io))
                }
            });

        ::tonic::transport::Endpoint::try_from("http://[::1]:50051/pseudo-endpoint")
            .unwrap()
            .connect_with_connector(client_connector)
            .await
            .unwrap()
    }
}

#[macro_export]
macro_rules! mock_server_fn {
    ($vis:vis $fn_name:ident; $($svc:ident),+; $logger:path) => {

        $vis async fn $fn_name(
            $($svc: impl $crate::Svc<Future: Send>),+
        ) -> (impl ::futures::Future<Output = ()>, $crate::EndpointMock) {
            use $logger::{info};

            let (connection_sender, connections_receiver) = ::tokio::sync::mpsc::channel(32);
            let incoming_connections: ::tokio_stream::wrappers::ReceiverStream<
                Result<::tokio::io::DuplexStream, ::tonic::transport::Error>,
            > = ::tokio_stream::wrappers::ReceiverStream::from(connections_receiver);

            let router = ::tonic::transport::Server::builder()$(
                .add_service($svc))+;

            // Spawn server
            let server_future = async move {
                info!("start grpc server");
                router
                    .serve_with_incoming(incoming_connections)
                    .await
                    .unwrap();
                info!("grpc server stopped");
            };

            let connector = $crate::EndpointMock::new(connection_sender);

            (server_future, connector)
        }
    };

    ($fn_name:ident; $($svc:ident),+; $logger:path) => {
        mock_server_fn!(pub(crate) $fn_name; $($svc),+; $logger);
    };

    ($vis:vis $fn_name:ident; $($svc:ident),+) => {
        mock_server_fn!($vis $fn_name; $($svc),+; ::tracing);
    };

    ($fn_name:ident; $($svc:ident),+) => {
        mock_server_fn!(pub(crate) $fn_name; $($svc),+; ::tracing);
    };
}