starbase 0.11.2

Framework for building performant command line applications and developer tools.
Documentation
#![allow(dead_code)]

use async_trait::async_trait;
use miette::{IntoDiagnostic, bail};
use starbase::{App, AppPhase, AppResult, AppSession};
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::task;

#[derive(Clone, Debug, Default)]
struct TestSession {
    pub contexts: Arc<RwLock<Vec<String>>>,
    pub order: Arc<RwLock<Vec<String>>>,
    pub error_in_phase: Option<AppPhase>,
    pub exit_in_phase: Option<AppPhase>,
}

impl TestSession {
    pub fn get_contexts(self) -> Vec<String> {
        let lock = Arc::into_inner(self.contexts).unwrap();
        lock.into_inner()
    }

    pub fn get_order(self) -> Vec<String> {
        let lock = Arc::into_inner(self.order).unwrap();
        lock.into_inner()
    }
}

#[async_trait]
impl AppSession for TestSession {
    async fn startup(&mut self) -> AppResult {
        dbg!(1);

        self.order.write().await.push("startup".into());

        if self.error_in_phase == Some(AppPhase::Startup) {
            bail!("error in startup");
        }

        if self.exit_in_phase == Some(AppPhase::Startup) {
            return Ok(Some(1));
        }

        Ok(None)
    }

    async fn analyze(&mut self) -> AppResult {
        dbg!(2);

        self.order.write().await.push("analyze".into());

        if self.error_in_phase == Some(AppPhase::Analyze) {
            bail!("error in analyze");
        }

        if self.exit_in_phase == Some(AppPhase::Analyze) {
            return Ok(Some(2));
        }

        Ok(None)
    }

    async fn execute(&mut self) -> AppResult {
        dbg!(3);

        self.order.write().await.push("execute".into());

        if self.error_in_phase == Some(AppPhase::Execute) {
            bail!("error in execute");
        }

        if self.exit_in_phase == Some(AppPhase::Execute) {
            return Ok(Some(3));
        }

        let context = self.contexts.clone();

        context.write().await.push("execute".into());

        task::spawn(async move {
            context.write().await.push("async-task".into());
        })
        .await
        .into_diagnostic()?;

        Ok(None)
    }

    async fn shutdown(&mut self) -> AppResult {
        dbg!(4);

        self.order.write().await.push("shutdown".into());

        if self.error_in_phase == Some(AppPhase::Shutdown) {
            bail!("error in shutdown");
        }

        if self.exit_in_phase == Some(AppPhase::Shutdown) {
            return Ok(Some(4));
        }

        self.contexts.write().await.push("shutdown".into());

        Ok(None)
    }
}

async fn noop<S>(_session: S) -> AppResult {
    Ok(None)
}

async fn noop_code<S>(_session: S) -> AppResult {
    Ok(Some(5))
}

#[tokio::test]
async fn runs_in_order() {
    let mut session = TestSession::default();

    App::default()
        .run_with_session(&mut session, noop)
        .await
        .unwrap();

    assert_eq!(
        session.get_order(),
        vec!["startup", "analyze", "execute", "shutdown"]
    );
}

#[tokio::test]
async fn runs_other_contexts() {
    let mut session = TestSession::default();

    App::default()
        .run_with_session(&mut session, noop)
        .await
        .unwrap();

    assert_eq!(
        session.get_contexts(),
        vec!["execute", "async-task", "shutdown"]
    );
}

mod startup {
    use super::*;

    #[tokio::test]
    async fn bubbles_up_error() {
        let mut session = TestSession {
            error_in_phase: Some(AppPhase::Startup),
            ..Default::default()
        };

        let error = App::default().run_with_session(&mut session, noop).await;

        assert!(error.is_err());
        assert_eq!(error.unwrap_err().to_string(), "error in startup");
        assert_eq!(session.get_order(), vec!["startup", "shutdown"]);
    }

    #[tokio::test]
    async fn returns_exit_code_1() {
        let mut session = TestSession {
            exit_in_phase: Some(AppPhase::Startup),
            ..Default::default()
        };

        let code = App::default()
            .run_with_session(&mut session, noop)
            .await
            .unwrap();

        assert_eq!(code, 1);
    }
}

mod analyze {
    use super::*;

    #[tokio::test]
    async fn bubbles_up_error() {
        let mut session = TestSession {
            error_in_phase: Some(AppPhase::Analyze),
            ..Default::default()
        };

        let error = App::default().run_with_session(&mut session, noop).await;

        assert!(error.is_err());
        assert_eq!(error.unwrap_err().to_string(), "error in analyze");
        assert_eq!(session.get_order(), vec!["startup", "analyze", "shutdown"]);
    }

    #[tokio::test]
    async fn returns_exit_code_2() {
        let mut session = TestSession {
            exit_in_phase: Some(AppPhase::Analyze),
            ..Default::default()
        };

        let code = App::default()
            .run_with_session(&mut session, noop)
            .await
            .unwrap();

        assert_eq!(code, 2);
    }
}

mod execute {
    use super::*;

    #[tokio::test]
    async fn bubbles_up_error() {
        let mut session = TestSession {
            error_in_phase: Some(AppPhase::Execute),
            ..Default::default()
        };

        let error = App::default().run_with_session(&mut session, noop).await;

        assert!(error.is_err());
        assert_eq!(error.unwrap_err().to_string(), "error in execute");
        assert_eq!(
            session.get_order(),
            vec!["startup", "analyze", "execute", "shutdown"]
        );
    }

    #[tokio::test]
    async fn returns_exit_code_3() {
        let mut session = TestSession {
            exit_in_phase: Some(AppPhase::Execute),
            ..Default::default()
        };

        let code = App::default()
            .run_with_session(&mut session, noop)
            .await
            .unwrap();

        assert_eq!(code, 3);
    }

    #[tokio::test]
    async fn returns_exit_code_5() {
        let mut session = TestSession::default();

        let code = App::default()
            .run_with_session(&mut session, noop_code)
            .await
            .unwrap();

        assert_eq!(code, 5);
    }
}

mod shutdown {
    use super::*;

    #[tokio::test]
    async fn bubbles_up_error() {
        let mut session = TestSession {
            error_in_phase: Some(AppPhase::Shutdown),
            ..Default::default()
        };

        let error = App::default().run_with_session(&mut session, noop).await;

        assert!(error.is_err());
        assert_eq!(error.unwrap_err().to_string(), "error in shutdown");
        assert_eq!(
            session.get_order(),
            vec!["startup", "analyze", "execute", "shutdown"]
        );
    }

    #[tokio::test]
    async fn returns_exit_code_4() {
        let mut session = TestSession {
            exit_in_phase: Some(AppPhase::Shutdown),
            ..Default::default()
        };

        let code = App::default()
            .run_with_session(&mut session, noop)
            .await
            .unwrap();

        assert_eq!(code, 4);
    }
}