presenterm 0.16.1

A terminal slideshow presentation tool
use crate::{
    code::{
        execute::{ExecutionHandle, LanguageSnippetExecutor, ProcessStatus},
        snippet::{ExpectedSnippetExecutionResult, Snippet},
    },
    render::operation::{
        AsRenderOperations, Pollable, PollableState, RenderAsync, RenderAsyncStartPolicy, RenderOperation,
    },
};
use std::{
    mem,
    ops::DerefMut,
    sync::{Arc, Mutex},
};

#[derive(Debug)]
pub(crate) struct ValidateSnippetOperation {
    snippet: Snippet,
    executor: LanguageSnippetExecutor,
    state: Arc<Mutex<State>>,
}

impl ValidateSnippetOperation {
    pub(crate) fn new(snippet: Snippet, executor: LanguageSnippetExecutor) -> Self {
        Self { snippet, executor, state: Default::default() }
    }
}

impl AsRenderOperations for ValidateSnippetOperation {
    fn as_render_operations(&self, _dimensions: &crate::WindowSize) -> Vec<RenderOperation> {
        vec![]
    }
}

impl RenderAsync for ValidateSnippetOperation {
    fn pollable(&self) -> Box<dyn Pollable> {
        Box::new(OperationPollable {
            snippet: self.snippet.clone(),
            executor: self.executor.clone(),
            state: self.state.clone(),
        })
    }

    fn start_policy(&self) -> RenderAsyncStartPolicy {
        RenderAsyncStartPolicy::Automatic
    }
}

#[derive(Debug, Default)]
enum State {
    #[default]
    Initial,
    Running(ExecutionHandle),
    Done(PollableState),
}

struct OperationPollable {
    snippet: Snippet,
    executor: LanguageSnippetExecutor,
    state: Arc<Mutex<State>>,
}

impl OperationPollable {
    fn success_to_pollable_state(&self) -> PollableState {
        match self.snippet.attributes.expected_execution_result {
            ExpectedSnippetExecutionResult::Success => PollableState::Done,
            ExpectedSnippetExecutionResult::Failure => {
                PollableState::Failed { error: "expected snippet to fail but it succeeded".into() }
            }
        }
    }

    fn error_to_pollable_state<S: Into<String>>(&self, error: S) -> PollableState {
        match self.snippet.attributes.expected_execution_result {
            ExpectedSnippetExecutionResult::Success => PollableState::Failed { error: error.into() },
            ExpectedSnippetExecutionResult::Failure => PollableState::Done,
        }
    }
}

impl Pollable for OperationPollable {
    fn poll(&mut self) -> PollableState {
        let mut state = self.state.lock().expect("lock poisoned");
        let next_state = match mem::take(state.deref_mut()) {
            State::Initial => match self.executor.execute_async(&self.snippet) {
                Ok(handle) => State::Running(handle),
                Err(e) => State::Done(self.error_to_pollable_state(e.to_string())),
            },
            State::Running(handle) => {
                let state = handle.state.lock().expect("lock poisoned");
                match state.status {
                    ProcessStatus::Running => {
                        drop(state);
                        State::Running(handle)
                    }
                    ProcessStatus::Success => State::Done(self.success_to_pollable_state()),
                    ProcessStatus::Failure => {
                        State::Done(self.error_to_pollable_state(String::from_utf8_lossy(&state.output)))
                    }
                }
            }
            State::Done(output) => State::Done(output),
        };
        *state = next_state;
        match &*state {
            State::Initial | State::Running(_) => PollableState::Unmodified,
            State::Done(output) => output.clone(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::code::{
        execute::SnippetExecutor,
        snippet::{SnippetAttributes, SnippetLanguage},
    };
    use rstest::rstest;

    #[rstest]
    #[case::success("fn main() { println!(\"hi\"); }", ExpectedSnippetExecutionResult::Success)]
    #[case::failure("fn main() ", ExpectedSnippetExecutionResult::Failure)]
    fn expectation_matches(#[case] contents: &str, #[case] expected_execution_result: ExpectedSnippetExecutionResult) {
        let snippet = Snippet {
            contents: contents.into(),
            language: SnippetLanguage::Rust,
            attributes: SnippetAttributes { expected_execution_result, ..Default::default() },
        };
        let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
        let state = Arc::new(Mutex::new(State::default()));
        let mut pollable =
            OperationPollable { snippet: snippet.clone(), executor: executor.clone(), state: state.clone() };
        loop {
            match pollable.poll() {
                PollableState::Unmodified | PollableState::Modified => continue,
                PollableState::Done => break,
                PollableState::Failed { error } => panic!("finished with error: {error}"),
            }
        }
        let mut pollable = OperationPollable { snippet, executor, state: state.clone() };
        assert!(matches!(pollable.poll(), PollableState::Done), "different pollable returned different");
    }

    #[rstest]
    #[case::success("fn main() { println!(\"hi\"); }", ExpectedSnippetExecutionResult::Failure)]
    #[case::failure("fn main() ", ExpectedSnippetExecutionResult::Success)]
    fn expect_does_not_match(
        #[case] contents: &str,
        #[case] expected_execution_result: ExpectedSnippetExecutionResult,
    ) {
        let snippet = Snippet {
            contents: contents.into(),
            language: SnippetLanguage::Rust,
            attributes: SnippetAttributes { expected_execution_result, ..Default::default() },
        };
        let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
        let state = Arc::new(Mutex::new(State::default()));
        let mut pollable =
            OperationPollable { snippet: snippet.clone(), executor: executor.clone(), state: state.clone() };
        loop {
            match pollable.poll() {
                PollableState::Unmodified | PollableState::Modified => continue,
                PollableState::Done => panic!("finished successfully"),
                PollableState::Failed { .. } => break,
            }
        }
        let mut pollable = OperationPollable { snippet, executor, state: state.clone() };
        assert!(matches!(pollable.poll(), PollableState::Failed { .. }), "different pollable returned different");
    }
}