thlorenz / rid

Rust integrated Dart framework providing an easy way to build Flutter apps with Rust.
63 stars 4 forks source link

Testing rid store rust code without Flutter #15

Open chertov opened 3 years ago

chertov commented 3 years ago

I'm trying to find a way how to test code with rid store, but without Flutter. My demo is little bit ugly.. Is there a more convenient way, perhaps? It may be worth considering adding more convenient testing tools in the future?

use rid::RidStore;

#[cfg(not(test))]
macro_rules! rid_post {
    ($t:tt) =>      ({ let val = $t; rid::post(val) });
    ($t:expr) =>    ({ let val = $t; rid::post(val) });
    ($t:block) =>   ({ let val = $t; rid::post(val) });
}
#[cfg(test)]
macro_rules! rid_post {
    ($t:tt) =>      ({ let val = $t; crate::store::tests::rid_post(val) });
    ($t:expr) =>    ({ let val = $t; crate::store::tests::rid_post(val) });
    ($t:block) =>   ({ let val = $t; crate::store::tests::rid_post(val) });
}

#[rid::model]
#[derive(Debug, Clone, PartialEq)]
pub enum ConnState {
    Connected,
    Disconnected
}

#[rid::store]
#[rid::enums(ConnState)]
#[derive(Debug, Clone)]
pub struct Store {
    running: bool,
    count: u32,
    connection_state: ConnState,
}

impl RidStore<Msg> for Store {
    fn create() -> Self {
        std::thread::spawn(|| {
            while store::read().running {
                std::thread::sleep(std::time::Duration::from_secs(1));
                store::write().connection_state = ConnState::Connected;
                rid_post!(Reply::ConnectionStateUpdate);
                std::thread::sleep(std::time::Duration::from_secs(1));
                store::write().connection_state = ConnState::Disconnected;
                rid_post!(Reply::ConnectionStateUpdate);
            }
        });

        Self {
            running: true,
            count: 0,
            connection_state: ConnState::Disconnected,
        }
    }

    fn update(&mut self, req_id: u64, msg: Msg) {
        match msg {
            Msg::Inc => {
                self.count += 1;
                rid_post!(Reply::Increased(req_id));
            }
        }
    }
}

#[rid::message(Reply)]
pub enum Msg { Inc }

#[rid::reply]
#[derive(Debug, PartialEq)]
pub enum Reply {
    ConnectionStateUpdate,
    Increased(u64),
}

#[cfg(test)]
mod tests {
    use super::*;

    fn update(req_id: u64, msg: Msg) { store::write().update(req_id, msg) }
    fn get() -> Store { store::read().clone() }
    fn set() -> std::sync::RwLockWriteGuard<'static, Store, > { store::write() }

    #[tokio::test]
    async fn devices() -> Result<(), anyhow::Error> {
        let mut rx = {
            let (tx, rx) = futures::channel::mpsc::channel::<Reply>(5);
            RID_CHANNEL.write().replace(tx);
            rx
        };

        let req_id = 1;
        update(req_id, Msg::Inc);
        assert_eq!(rx.next().await.unwrap(), Reply::Increased(req_id));
        assert_eq!(get().count, 1);
        update(req_id, Msg::Inc);
        assert_eq!(rx.next().await.unwrap(), Reply::Increased(req_id));
        assert_eq!(get().count, 2);
        assert_eq!(rx.next().await.unwrap(), Reply::ConnectionStateUpdate);
        assert_eq!(get().connection_state, ConnState::Connected);
        assert_eq!(rx.next().await.unwrap(), Reply::ConnectionStateUpdate);
        assert_eq!(get().connection_state, ConnState::Disconnected);
        set().running = false;

        Ok(())
    }

    use std::sync::Arc;
    use parking_lot::RwLock;
    use futures::StreamExt;
    lazy_static! {
        static ref RID_CHANNEL: Arc<RwLock<Option<futures::channel::mpsc::Sender<Reply>>>> = Arc::new(RwLock::new(None));
    }
    pub fn rid_post(msg: Reply) {
        RID_CHANNEL.read().clone().unwrap().start_send(msg).unwrap();
    }
}
thlorenz commented 3 years ago

Sorry for replying late, I had lost track of my github notifications :)

This is an awesome idea. I hadn't thought about this as I'm currently testing rid itself with some integration tests which are basically mini apps. I'm testing them via Dart tests. If you're just trying to avoid having to run Flutter to run tests you could do something similar to that. Those tests are here.

However those run somewhat slow and I bet the ones you're creating run a lot faster. Also they allow you to debug your app since it's running rust directly (something that isn't currently possible when running things via dart).

So I totally love it!

Multiple Steps

1. Example Prototype

What I'd suggest is to create a sample app with those tests inside the rid-examples repo. Include all the boilerplate to make it work there and submit a PR. This can serve as an example for others to do the same.

2. Building this into Rid

Once we got this example we can look at what parts make sense to include with rid and expose via a testing API.

3. Simplify Example

Once we got that rid test API we can simplify the example to just use that instead.

Implementation

I'm not a huge fan of more macros especially since the rust-analyzer provides very limited intellisense for those still. If we can have functions instead, but we can discuss details with the prototype PR (which you should just file in draft mode once you even got the minimum working so we can iterate on the idea).

Also it'd be nice to not call update directly, but somehow use the messaging API instead. You basically just need to get a hold of the Isolate and communicate with that. A good starting point to see how this works is to run cargo expand in a simple example and see what message related code gets generated. That way the tests run even closer to how the app would run with Dart/Flutter.

So I'm looking forward to the initial implementation in the PR. It's better to discuss directly on code and that way I can also branch off and commit some ideas + help out.