stateright 0.29.0

A model checker for implementing distributed systems.
Documentation
use actix_web::{*, web::Json};
use crate::*;
use parking_lot::RwLock;
use serde::ser::{SerializeStruct, Serializer};
use serde::Serialize;
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::thread::{sleep, spawn};
use std::time::Duration;
use std::collections::VecDeque;

#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
struct StatusView {
    done: bool,
    model: String,
    state_count: usize,
    unique_state_count: usize,
    properties: Vec<(Expectation,
                     String,           // name
                     Option<String>)>, // encoded path to discovery
    recent_path: Option<String>,
}

#[derive(Debug, Eq, PartialEq)]
struct StateView<State> {
    action: Option<String>,
    outcome: Option<String>,
    state: Option<State>,
    svg: Option<String>,
}

type StateViewsJson<State> = Json<Vec<StateView<State>>>;

impl<State> serde::Serialize for StateView<State>
where
    State: Debug + Hash,
{
    fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
        let mut out = ser.serialize_struct("StateView", 3)?;
        if let Some(ref action) = self.action {
            out.serialize_field("action", action)?;
        }
        if let Some(ref outcome) = self.outcome {
            out.serialize_field("outcome", outcome)?;
        }
        if let Some(ref state) = self.state {
            out.serialize_field("state", &format!("{:#?}", state))?;
            out.serialize_field("fingerprint", &format!("{:?}", fingerprint(&state)))?;
        }
        if let Some(ref svg) = self.svg {
            out.serialize_field("svg", svg)?;
        }
        out.end()
    }
}

struct Snapshot<Action>(bool, Option<Vec<Action>>);
impl<M: Model> CheckerVisitor<M> for Arc<RwLock<Snapshot<M::Action>>> {
    fn visit(&self, _: &M, path: Path<M::State, M::Action>) {
        let guard = self.read();
        if !guard.0 { return }
        drop(guard);

        let mut guard = self.write();
        if !guard.0 { return } // May be racing other threads.
        guard.0 = false;
        guard.1 = Some(path.into_actions());
    }
}

pub(crate) fn serve<M>(checker_builder: CheckerBuilder<M>, addresses: impl ToSocketAddrs) -> Arc<impl Checker<M>>
where M: 'static + Model + Send + Sync,
      M::Action: Debug + Send + Sync,
      M::State: Debug + Hash + Send + Sync,
{
    let snapshot = Arc::new(RwLock::new(Snapshot(true, None)));
    let snapshot_for_visitor = Arc::clone(&snapshot);
    let snapshot_for_server = Arc::clone(&snapshot);
    spawn(move || {
        loop {
            sleep(Duration::from_secs(4));
            snapshot.write().0 = true;
        }
    });
    let checker = checker_builder
        .visitor(snapshot_for_visitor)
        .spawn_bfs();
    serve_checker(checker, snapshot_for_server, addresses)
}

fn serve_checker<M, C>(
    checker: C,
    snapshot: Arc<RwLock<Snapshot<M::Action>>>,
    addresses: impl ToSocketAddrs)
    -> Arc<impl Checker<M>>
where M: 'static + Model + Send + Sync,
      M::Action: Debug + Send + Sync,
      M::State: Debug + Hash + Send + Sync,
      C: 'static + Checker<M> + Send + Sync,
{
    let checker = Arc::new(checker);

    let data = Arc::new((snapshot, Arc::clone(&checker)));
    HttpServer::new(move || {
        macro_rules! get_ui_file {
            ($filename:literal) => {
                web::get().to(|| HttpResponse::Ok().body({
                    if let Ok(content) = std::fs::read(concat!("./ui/", $filename)) {
                        log::info!("Explorer dev mode. Loading {} from disk.", $filename);
                        content
                    } else {
                        include_bytes!(concat!("../../ui/", $filename)).to_vec()
                    }
                }))
            }
        }

        App::new()
            .data(Arc::clone(&data))
            .route("/.status", web::get().to(status::<M, C>))
            .route("/.states{fingerprints:.*}", web::get().to(states::<M, C>))
            .route("/", get_ui_file!("index.htm"))
            .route("/app.css", get_ui_file!("app.css"))
            .route("/app.js", get_ui_file!("app.js"))
            .route("/knockout-3.5.0.js", get_ui_file!("knockout-3.5.0.js"))
    }).bind(addresses).unwrap().run().unwrap();

    checker
}

type Data<Action, Checker> = web::Data<Arc<(Arc<RwLock<Snapshot<Action>>>, Arc<Checker>)>>;

fn status<M, C>(_: HttpRequest, data: Data<M::Action, C>) -> Json<StatusView>
where M: Model,
      M::Action: Debug,
      M::State: Hash,
      C: Checker<M>,
{
    let snapshot = &data.0;
    let checker = &data.1;

    let status = StatusView {
        model: std::any::type_name::<M>().to_string(),
        done: checker.is_done(),
        state_count: checker.state_count(),
        unique_state_count: checker.unique_state_count(),
        properties: checker.model().properties().into_iter()
            .map(|p| (
                    p.expectation,
                    p.name.to_string(),
                    checker.discovery(p.name).map(|p| p.encode()),
            ))
            .collect(),
        recent_path: snapshot.read().1.as_ref().map(|p| format!("{:?}", p)),
    };
    Json(status)
}

fn states<M, C>(req: HttpRequest, data: Data<M::Action, C>)
    -> Result<StateViewsJson<M::State>>
where M: Model,
      M::Action: Debug,
      M::State: Debug + Hash,
      C: Checker<M>,
{
    let model = &data.1.model();

    // extract fingerprints
    let mut fingerprints_str = req.match_info().get("fingerprints").expect("missing 'fingerprints' param").to_string();
    if fingerprints_str.ends_with('/') {
        let relevant_len = fingerprints_str.len() - 1;
        fingerprints_str.truncate(relevant_len);
    }
    let fingerprints: VecDeque<_> = fingerprints_str.split('/').filter_map(|fp| fp.parse::<Fingerprint>().ok()).collect();

    // ensure all but the first string (which is empty) were parsed
    if fingerprints.len() + 1 != fingerprints_str.split('/').count() {
        return Err(
            actix_web::error::ErrorNotFound(
                format!("Unable to parse fingerprints {}", fingerprints_str)));
    }

    // now build up all the subsequent `StateView`s
    let mut results = Vec::new();
    if fingerprints.is_empty() {
        for state in model.init_states() {
            let svg = {
                let mut fingerprints: VecDeque<_> = fingerprints.clone().into_iter().collect();
                fingerprints.push_back(fingerprint(&state));
                model.as_svg(Path::from_fingerprints::<M>(model, fingerprints))
            };
            results.push(StateView {
                action: None,
                outcome: None,
                state: Some(state),
                svg,
            });
        }
    } else if let Some(last_state) = Path::final_state::<M>(model, fingerprints.clone()) {
        // Must generate the actions three times because they are consumed by `next_state`
        // and `display_outcome`.
        let mut actions1 = Vec::new();
        let mut actions2 = Vec::new();
        let mut actions3 = Vec::new();
        model.actions(&last_state, &mut actions1);
        model.actions(&last_state, &mut actions2);
        model.actions(&last_state, &mut actions3);
        for ((action, action2), action3) in actions1.into_iter().zip(actions2).zip(actions3) {
            let outcome = model.format_step(&last_state, action2);
            let state = model.next_state(&last_state, action3);
            if let Some(state) = state {
                let svg = {
                    let mut fingerprints: VecDeque<_> = fingerprints.clone().into_iter().collect();
                    fingerprints.push_back(fingerprint(&state));
                    model.as_svg(Path::from_fingerprints::<M>(model, fingerprints))
                };
                results.push(StateView {
                    action: Some(model.format_action(&action)),
                    outcome,
                    state: Some(state),
                    svg,
                });
            } else {
                // "Action ignored" case is still returned, as it may be useful for debugging.
                results.push(StateView {
                    action: Some(model.format_action(&action)),
                    outcome: None,
                    state: None,
                    svg: None,
                });
            }
        }
    } else {
        return Err(
            actix_web::error::ErrorNotFound(
                format!("Unable to find state following fingerprints {}", fingerprints_str)));
    }

    Ok(Json(results))
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::test_util::binary_clock::*;
    use lazy_static::lazy_static;

    #[test]
    fn can_init() {
        let checker = Arc::new(BinaryClock.checker().spawn_bfs());
        assert_eq!(get_states(Arc::clone(&checker), "/").unwrap(), vec![
            StateView { action: None, outcome: None, state: Some(0), svg: None },
            StateView { action: None, outcome: None, state: Some(1), svg: None },
        ]);
    }

    #[test]
    fn can_next() {
        let checker = Arc::new(BinaryClock.checker().spawn_bfs());
        // We need a static string for TestRequest, so this is precomputed, but you can recompute
        // the values if needed as follows:
        // ```
        // let first = fingerprint(&1_i8);
        // let second = fingerprint(&0_i8);
        // let path_name = format!("/{}/{}", first, second);
        // println!("New path name is: {}", path_name);
        // ```
        assert_eq!(get_states(Arc::clone(&checker), "/2716592049047647680/9080728272894440685").unwrap(), vec![
            StateView {
                action: Some("GoHigh".to_string()),
                outcome: Some("1".to_string()),
                state: Some(1),
                svg: None,
            },
        ]);
    }

    #[test]
    fn err_for_invalid_fingerprint() {
        let checker = Arc::new(BinaryClock.checker().spawn_bfs());
        assert_eq!(format!("{}", get_states(Arc::clone(&checker), "/one/two/three").unwrap_err()),
            "Unable to parse fingerprints /one/two/three");
        assert_eq!(format!("{}", get_states(Arc::clone(&checker), "/1/2/3").unwrap_err()),
            "Unable to find state following fingerprints /1/2/3");
    }

    #[test]
    fn smoke_test_states() {
        use crate::actor::{ActorModelState, Envelope, Id, LossyNetwork, Network};
        use crate::actor::actor_test_util::ping_pong::{PingPongCfg, PingPongMsg::*};

        let checker = Arc::new(
            PingPongCfg {
                max_nat: 2,
                maintains_history: true,
            }
            .into_model()
            .init_network(Network::new_unordered_nonduplicating([]))
            .lossy_network(LossyNetwork::Yes)
            .checker()
            .spawn_bfs());
        assert_eq!(
            get_states(Arc::clone(&checker), "/").unwrap(),
            vec![
                StateView {
                    action: None,
                    outcome: None,
                    state: Some(ActorModelState {
                        actor_states: vec![Arc::new(0), Arc::new(0)],
                        history: (0, 1),
                        is_timer_set: vec![false,false],
                        network: Network::new_unordered_nonduplicating([
                            Envelope { src: Id::from(0), dst: Id::from(1), msg: Ping(0) },
                        ]),
                    }),
                    svg: Some("<svg version=\'1.1\' baseProfile=\'full\' width=\'500\' height=\'30\' viewbox=\'-20 -20 520 50\' xmlns=\'http://www.w3.org/2000/svg\'><defs><marker class=\'svg-event-shape\' id=\'arrow\' markerWidth=\'12\' markerHeight=\'10\' refX=\'12\' refY=\'5\' orient=\'auto\'><polygon points=\'0 0, 12 5, 0 10\' /></marker></defs><line x1=\'0\' y1=\'0\' x2=\'0\' y2=\'30\' class=\'svg-actor-timeline\' />\n<text x=\'0\' y=\'0\' class=\'svg-actor-label\'>0</text>\n<line x1=\'100\' y1=\'0\' x2=\'100\' y2=\'30\' class=\'svg-actor-timeline\' />\n<text x=\'100\' y=\'0\' class=\'svg-actor-label\'>1</text>\n</svg>\n".to_string()),
                },
            ]);

        lazy_static! {
            static ref PATH: String = {
                use crate::actor::actor_test_util::ping_pong::{PingPongActor, PingPongHistory};
                let fp = fingerprint(&ActorModelState::<PingPongActor, PingPongHistory> {
                    actor_states: vec![Arc::new(0), Arc::new(0)],
                    history: (0, 1),
                    is_timer_set: vec![false,false],
                    network: Network::new_unordered_nonduplicating([
                        Envelope { src: Id::from(0), dst: Id::from(1), msg: Ping(0) },
                    ]),
                });
                format!("/{}", fp)
            };
        }
        let states = get_states(Arc::clone(&checker), PATH.as_ref()).unwrap();
        assert_eq!(states.len(), 2);
        assert_eq!(
            states[0],
            StateView {
                action: Some("Drop(Envelope { src: Id(0), dst: Id(1), msg: Ping(0) })".to_string()),
                outcome: Some("DROP: Envelope { src: Id(0), dst: Id(1), msg: Ping(0) }".to_string()),
                state: Some(ActorModelState {
                    actor_states: vec![Arc::new(0), Arc::new(0)],
                    history: (0, 1),
                    is_timer_set: vec![false,false],
                    network: Network::new_unordered_nonduplicating([]),
                }),
                svg: Some("<svg version='1.1' baseProfile='full' width='500' height='60' viewbox='-20 -20 520 80' xmlns='http://www.w3.org/2000/svg'><defs><marker class='svg-event-shape' id='arrow' markerWidth='12' markerHeight='10' refX='12' refY='5' orient='auto'><polygon points='0 0, 12 5, 0 10' /></marker></defs><line x1='0' y1='0' x2='0' y2='60' class='svg-actor-timeline' />\n<text x='0' y='0' class='svg-actor-label'>0</text>\n<line x1='100' y1='0' x2='100' y2='60' class='svg-actor-timeline' />\n<text x='100' y='0' class='svg-actor-label'>1</text>\n</svg>\n".to_string()),
            });
        assert_eq!(
            states[1],
            StateView {
                action: Some("Id(0) → Ping(0) → Id(1)".to_string()),
                outcome: Some("OUT: [Send(Id(0), Pong(0))]\n\nNEXT_STATE: 1\n\nPREV_STATE: 0\n".to_string()),
                state: Some(ActorModelState {
                    actor_states: vec![
                        Arc::new(0),
                        Arc::new(1),
                    ],
                    history: (1, 2),
                    is_timer_set: vec![false,false],
                    network: Network::new_unordered_nonduplicating([
                        Envelope { src: Id::from(1), dst: Id::from(0), msg: Pong(0) },
                    ]),
                }),
                svg: Some("<svg version='1.1' baseProfile='full' width='500' height='60' viewbox='-20 -20 520 80' xmlns='http://www.w3.org/2000/svg'><defs><marker class='svg-event-shape' id='arrow' markerWidth='12' markerHeight='10' refX='12' refY='5' orient='auto'><polygon points='0 0, 12 5, 0 10' /></marker></defs><line x1='0' y1='0' x2='0' y2='60' class='svg-actor-timeline' />\n<text x='0' y='0' class='svg-actor-label'>0</text>\n<line x1='100' y1='0' x2='100' y2='60' class='svg-actor-timeline' />\n<text x='100' y='0' class='svg-actor-label'>1</text>\n<line x1='0' x2='100' y1='0' y2='30' marker-end='url(#arrow)' class='svg-event-line' />\n<text x='100' y='30' class='svg-event-label'>Ping(0)</text>\n</svg>\n".to_string()),
            });
    }

    #[test]
    fn smoke_test_status() {
        use crate::actor::{LossyNetwork, Network};
        use crate::actor::actor_test_util::ping_pong::PingPongCfg;

        let snapshot = Arc::new(RwLock::new(Snapshot(true, None)));
        let checker = PingPongCfg {
                max_nat: 2,
                maintains_history: true,
            }
            .into_model()
            .init_network(Network::new_unordered_nonduplicating([]))
            .lossy_network(LossyNetwork::No)
            .checker()
            .visitor(Arc::clone(&snapshot)).spawn_bfs().join();
        let status = get_status(Arc::new(checker), snapshot).unwrap();
        assert_eq!(status.done, true);
        assert_eq!(
            status.model,
            "stateright::actor::model::ActorModel<\
                 stateright::actor::actor_test_util::ping_pong::PingPongActor, \
                 stateright::actor::actor_test_util::ping_pong::PingPongCfg, (u32, u32)>");
        assert_eq!(status.state_count, 5);
        assert_eq!(status.unique_state_count, 5);
        let assert_discovery =
            |status: &StatusView,
             expectation: Expectation,
             name: &'static str,
             has_discovery: bool|
        {
            let match_found = status.properties.iter()
                .any(|(e, n, d)| {
                    e == &expectation && n == name && d.is_some() == has_discovery
                });
            if !match_found {
                panic!("Not found. expectation={:?}, name={:?}, has_discovery={:?}, properties={:#?}",
                       expectation, name, has_discovery, status.properties);
            }
        };
        assert_discovery(&status, Expectation::Always,     "delta within 1",  false);
        assert_discovery(&status, Expectation::Sometimes,  "can reach max",   true);
        assert_discovery(&status, Expectation::Eventually, "must reach max",  false);
        assert_discovery(&status, Expectation::Eventually, "must exceed max", true);
        assert_discovery(&status, Expectation::Always,     "#in <= #out",     false);
        assert_discovery(&status, Expectation::Eventually, "#out <= #in + 1", false);
        assert!(status.recent_path.unwrap().starts_with("["));
    }

    fn get_states<M, C>(checker: Arc<C>, path_name: &'static str)
                -> Result<Vec<StateView<M::State>>>
    where M: Model,
          M::Action: Debug,
          M::State: Debug + Hash,
          C: Checker<M>,
    {
        let req = actix_web::test::TestRequest::get()
            .param("fingerprints", &path_name)
            .to_http_request();
        let snapshot = Arc::new(RwLock::new(Snapshot(true, None)));
        let data = web::Data::new(Arc::new((snapshot, checker)));
        match states(req, data) {
            Ok(Json(view)) => Ok(view),
            Err(err) => Err(err),
        }
    }

    fn get_status<M, C>(checker: Arc<C>, snapshot: Arc<RwLock<Snapshot<M::Action>>>)
    -> Result<StatusView>
    where M: Model,
          M::Action: Debug,
          M::State: Debug + Hash,
          C: Checker<M>,
    {
        let req = actix_web::test::TestRequest::get().to_http_request();
        let data = web::Data::new(Arc::new((snapshot, checker)));
        let Json(view) = status(req, data);
        Ok(view)
    }
}