diff options
author | Tim Oram <dev@mitmaro.ca> | 2021-07-05 10:58:46 -0230 |
---|---|---|
committer | Tim Oram <dev@mitmaro.ca> | 2021-07-05 16:27:53 -0230 |
commit | 772ba7e40a023f780054a6b689780bccd1b47e44 (patch) | |
tree | e9ee71b12d0788e0dbb7b021ed1ba8eb44262496 /src/core/process | |
parent | 6de625d1e1de3de86aa0544a0221c0644c2a40f3 (diff) |
Move process module into core
Diffstat (limited to 'src/core/process')
-rw-r--r-- | src/core/process/mod.rs | 202 | ||||
-rw-r--r-- | src/core/process/tests.rs | 487 |
2 files changed, 689 insertions, 0 deletions
diff --git a/src/core/process/mod.rs b/src/core/process/mod.rs new file mode 100644 index 0000000..c5429a7 --- /dev/null +++ b/src/core/process/mod.rs @@ -0,0 +1,202 @@ +#[cfg(test)] +mod tests; + +use std::{process::Command, thread}; + +use anyhow::{anyhow, Result}; +use display::Tui; +use input::{Event, EventHandler, MetaEvent}; +use todo_file::TodoFile; +use view::{spawn_view_thread, RenderContext, View, ViewSender}; + +use crate::module::{ExitStatus, Modules, ProcessResult, State}; + +pub struct Process { + event_handler: EventHandler, + exit_status: Option<ExitStatus>, + rebase_todo: TodoFile, + render_context: RenderContext, + state: State, + threads: Vec<thread::JoinHandle<()>>, + view_sender: ViewSender, +} + +impl Process { + pub(crate) fn new<C: Tui + Send + 'static>( + rebase_todo: TodoFile, + event_handler: EventHandler, + view: View<C>, + ) -> Self { + #[allow(deprecated)] + let view_size = view.get_view_size(); + let mut threads = vec![]; + + let (view_sender, view_thread) = spawn_view_thread(view); + threads.push(view_thread); + + Self { + event_handler, + exit_status: None, + rebase_todo, + render_context: RenderContext::new(view_size.width() as u16, view_size.height() as u16), + state: State::List, + threads, + view_sender, + } + } + + pub(crate) fn run(&mut self, mut modules: Modules) -> Result<ExitStatus> { + if self.view_sender.start().is_err() { + self.exit_status = Some(ExitStatus::StateError); + return Ok(ExitStatus::StateError); + } + + self.handle_process_result( + &mut modules, + &ProcessResult::new().event(Event::Resize( + self.render_context.width() as u16, + self.render_context.height() as u16, + )), + ); + self.activate(&mut modules, State::List); + while self.exit_status.is_none() { + let view_data = modules.build_view_data(self.state, &self.render_context, &self.rebase_todo); + if self.view_sender.render(view_data).is_err() { + self.exit_status = Some(ExitStatus::StateError); + continue; + } + loop { + if self.view_sender.is_poisoned() { + self.exit_status = Some(ExitStatus::StateError); + break; + } + let result = modules.handle_input( + self.state, + &self.event_handler, + &self.view_sender, + &mut self.rebase_todo, + ); + + if let Some(event) = result.event { + if event != Event::None { + self.handle_process_result(&mut modules, &result); + break; + } + } + } + } + if self.view_sender.stop().is_err() { + return Ok(ExitStatus::StateError); + } + if let Some(status) = self.exit_status { + eprintln!("Status: {:?}", status); + if status != ExitStatus::Kill { + self.rebase_todo.write_file()?; + } + } + + if self.view_sender.end().is_err() { + return Ok(ExitStatus::StateError); + } + + while let Some(handle) = self.threads.pop() { + if handle.join().is_err() { + return Ok(ExitStatus::StateError); + } + } + + Ok(self.exit_status.unwrap_or(ExitStatus::Good)) + } + + fn handle_process_result(&mut self, modules: &mut Modules, result: &ProcessResult) { + let previous_state = self.state; + + // render context and view_sender need a size update early + if let Some(Event::Resize(width, height)) = result.event { + self.view_sender.resize(width, height); + self.render_context.update(width, height); + } + + if let Some(exit_status) = result.exit_status { + self.exit_status = Some(exit_status); + } + + if let Some(ref error) = result.error { + self.state = State::Error; + modules.error(self.state, error); + self.activate(modules, result.state.unwrap_or(previous_state)); + } + else if let Some(new_state) = result.state { + if new_state != self.state { + modules.deactivate(self.state); + self.state = new_state; + self.activate(modules, previous_state); + } + } + + match result.event { + Some(Event::Meta(MetaEvent::Exit)) => { + self.exit_status = Some(ExitStatus::Abort); + }, + Some(Event::Meta(MetaEvent::Kill)) => { + self.exit_status = Some(ExitStatus::Kill); + }, + Some(Event::Resize(..)) => { + if self.state != State::WindowSizeError && self.render_context.is_window_too_small() { + self.state = State::WindowSizeError; + self.activate(modules, previous_state); + } + }, + _ => {}, + }; + + if let Some(ref external_command) = result.external_command { + match self.run_command(external_command) { + Ok(meta_event) => self.event_handler.push_event(Event::from(meta_event)), + Err(err) => { + self.handle_process_result( + modules, + &ProcessResult::new().state(State::List).error(err.context(format!( + "Unable to run {} {}", + external_command.0, + external_command.1.join(" ") + ))), + ); + }, + } + } + } + + fn run_command(&mut self, external_command: &(String, Vec<String>)) -> Result<MetaEvent> { + self.view_sender.stop()?; + + let mut cmd = Command::new(external_command.0.clone()); + cmd.args(external_command.1.clone()); + + let result = cmd + .status() + .map(|status| { + if status.success() { + MetaEvent::ExternalCommandSuccess + } + else { + MetaEvent::ExternalCommandError + } + }) + .map_err(|err| anyhow!(err)); + + self.view_sender.start()?; + + result + } + + fn activate(&mut self, modules: &mut Modules, previous_state: State) { + let result = modules.activate(self.state, &self.rebase_todo, previous_state); + // always trigger a resize on activate, for modules that track size + self.event_handler.push_event(Event::Resize( + self.render_context.width() as u16, + self.render_context.height() as u16, + )); + self.handle_process_result(modules, &result); + } +} diff --git a/src/core/process/tests.rs b/src/core/process/tests.rs new file mode 100644 index 0000000..75c3910 --- /dev/null +++ b/src/core/process/tests.rs @@ -0,0 +1,487 @@ +use std::{path::Path, sync::atomic::Ordering}; + +use anyhow::anyhow; +use config::testutil::create_theme; +use display::{testutil::CrossTerm, Display, Size}; +use input::InputOptions; +use view::{assert_rendered_output, ViewData}; + +use super::*; +use crate::{ + error::Error, + module::{testutil::module_test, Module}, + window_size_error::WindowSizeError, +}; + +struct TestModule { + pub event_callback: Box<dyn Fn(&EventHandler, &ViewSender, &mut TodoFile) -> ProcessResult>, + pub view_data: ViewData, + pub view_data_callback: Box<dyn Fn(&mut ViewData)>, +} + +impl TestModule { + fn new() -> Self { + Self { + event_callback: Box::new(|_, _, _| ProcessResult::new().event(Event::from(MetaEvent::Kill))), + view_data: ViewData::new(|_| {}), + view_data_callback: Box::new(|_| {}), + } + } +} + +impl Module for TestModule { + fn build_view_data(&mut self, _render_context: &RenderContext, _rebase_todo: &TodoFile) -> &ViewData { + (self.view_data_callback)(&mut self.view_data); + &self.view_data + } + + fn handle_events( + &mut self, + event_handler: &EventHandler, + view_sender: &ViewSender, + todo_file: &mut TodoFile, + ) -> ProcessResult { + (self.event_callback)(event_handler, view_sender, todo_file) + } +} + +fn create_crossterm() -> CrossTerm { + let mut crossterm = CrossTerm::new(); + crossterm.set_size(Size::new(100, 300)); + crossterm +} + +fn create_modules() -> Modules { + let mut modules = Modules::new(); + modules.register_module(State::Error, Error::new()); + modules.register_module(State::WindowSizeError, WindowSizeError::new()); + modules +} + +#[test] +fn view_start_error() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let mut modules = create_modules(); + modules.register_module(State::List, TestModule::new()); + while process.view_sender.end().is_ok() {} + assert_eq!(process.run(modules).unwrap(), ExitStatus::StateError); + }); +} + +#[test] +fn window_too_small_on_start() { + module_test(&["pick aaa comment"], &[Event::from(MetaEvent::Exit)], |test_context| { + let mut crossterm = create_crossterm(); + crossterm.set_size(Size::new(1, 1)); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let modules = create_modules(); + process.run(modules).unwrap(); + assert_eq!(process.state, State::WindowSizeError); + }); +} + +#[test] +fn render_error() { + module_test(&["pick aaa comment"], &[Event::from(MetaEvent::Exit)], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let mut modules = create_modules(); + let mut test_module = TestModule::new(); + let sender = process.view_sender.clone(); + test_module.view_data_callback = Box::new(move |_| while sender.end().is_ok() {}); + modules.register_module(State::List, test_module); + assert_eq!(process.run(modules).unwrap(), ExitStatus::StateError); + }); +} + +#[test] +fn view_sender_is_poisoned() { + module_test(&["pick aaa comment"], &[Event::from(MetaEvent::Exit)], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let mut modules = create_modules(); + let test_module = TestModule::new(); + process.view_sender.clone_poisoned().store(true, Ordering::Relaxed); + modules.register_module(State::List, test_module); + assert_eq!(process.run(modules).unwrap(), ExitStatus::StateError); + }); +} + +#[test] +fn stop_error() { + module_test(&["pick aaa comment"], &[Event::from(MetaEvent::Exit)], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let mut modules = create_modules(); + let mut test_module = TestModule::new(); + let sender = process.view_sender.clone(); + test_module.event_callback = Box::new(move |_, _, _| { + while sender.end().is_ok() {} + ProcessResult::new().event(Event::from(MetaEvent::Exit)) + }); + modules.register_module(State::List, test_module); + assert_eq!(process.run(modules).unwrap(), ExitStatus::StateError); + }); +} + +#[test] +fn handle_exit_event_that_is_not_kill() { + module_test(&["pick aaa comment"], &[], |mut test_context| { + test_context.rebase_todo_file.write_file().unwrap(); + test_context.rebase_todo_file.set_lines(vec![]); + let mut shadow_rebase_file = test_context.new_todo_file(); + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let mut modules = create_modules(); + let mut test_module = TestModule::new(); + test_module.event_callback = Box::new(|_, _, _| { + ProcessResult::new() + .event(Event::from(MetaEvent::Rebase)) + .exit_status(ExitStatus::Good) + }); + modules.register_module(State::List, test_module); + assert_eq!(process.run(modules).unwrap(), ExitStatus::Good); + shadow_rebase_file.load_file().unwrap(); + assert!(shadow_rebase_file.is_empty()); + }); +} + +#[test] +fn handle_exit_event_that_is_kill() { + module_test(&["pick aaa comment"], &[], |mut test_context| { + test_context.rebase_todo_file.write_file().unwrap(); + test_context.rebase_todo_file.set_lines(vec![]); + let mut shadow_rebase_file = test_context.new_todo_file(); + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let mut modules = create_modules(); + let mut test_module = TestModule::new(); + test_module.event_callback = Box::new(|_, _, _| { + ProcessResult::new() + .event(Event::from(MetaEvent::Kill)) + .exit_status(ExitStatus::Kill) + }); + modules.register_module(State::List, test_module); + assert_eq!(process.run(modules).unwrap(), ExitStatus::Kill); + shadow_rebase_file.load_file().unwrap(); + assert!(!shadow_rebase_file.is_empty()); + }); +} + +#[test] +fn handle_process_result_error() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let mut modules = create_modules(); + let result = ProcessResult::new().error(anyhow!("Test error")); + process.handle_process_result(&mut modules, &result); + assert_eq!(process.state, State::Error); + }); +} + +#[test] +fn handle_process_result_new_state() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let mut modules = create_modules(); + modules.register_module(State::List, TestModule::new()); + let result = ProcessResult::new().state(State::Error); + process.handle_process_result(&mut modules, &result); + assert_eq!(process.state, State::Error); + }); +} + +#[test] +fn handle_process_result_state_same() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let mut modules = create_modules(); + modules.register_module(State::List, TestModule::new()); + let result = ProcessResult::new().state(State::List); + process.handle_process_result(&mut modules, &result); + assert_eq!(process.state, State::List); + }); +} + +#[test] +fn handle_process_result_exit_event() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut modules = create_modules(); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let result = ProcessResult::new().event(Event::from(MetaEvent::Exit)); + process.handle_process_result(&mut modules, &result); + assert_eq!(process.exit_status, Some(ExitStatus::Abort)); + }); +} + +#[test] +fn handle_process_result_kill_event() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut modules = create_modules(); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let result = ProcessResult::new().event(Event::from(MetaEvent::Kill)); + process.handle_process_result(&mut modules, &result); + assert_eq!(process.exit_status, Some(ExitStatus::Kill)); + }); +} + +#[test] +fn handle_process_result_resize_event_not_too_small() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut modules = create_modules(); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let result = ProcessResult::new().event(Event::Resize(100, 200)); + process.handle_process_result(&mut modules, &result); + assert_eq!(process.render_context.width(), 100); + assert_eq!(process.render_context.height(), 200); + assert_eq!(process.state, State::List); + }); +} +#[test] +fn handle_process_result_resize_event_too_small() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut modules = create_modules(); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let result = ProcessResult::new().event(Event::Resize(10, 20)); + process.handle_process_result(&mut modules, &result); + assert_eq!(process.render_context.width(), 10); + assert_eq!(process.render_context.height(), 20); + assert_eq!(process.state, State::WindowSizeError); + }); +} + +#[test] +fn handle_process_result_other_event() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let mut modules = create_modules(); + let result = ProcessResult::new().event(Event::from('a')); + process.handle_process_result(&mut modules, &result); + assert_eq!(process.exit_status, None); + assert_eq!(process.state, State::List); + }); +} + +#[test] +fn handle_process_result_external_command_not_executable() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut modules = create_modules(); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let command = String::from( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test") + .join("not-executable.sh") + .to_str() + .unwrap(), + ); + let result = ProcessResult::new().external_command(command.clone(), vec![]); + process.handle_process_result(&mut modules, &result); + assert_eq!(process.state, State::Error); + let view_data = modules.build_view_data(process.state, &process.render_context, &process.rebase_todo); + assert_rendered_output!( + view_data, + "{TITLE}", + "{BODY}", + format!("{{Normal}}Unable to run {} ", command), + if cfg!(windows) { + "{Normal}%1 is not a valid Win32 application. (os error 193)" + } + else { + "{Normal}Permission denied (os error 13)" + }, + "{TRAILING}", + "{IndicatorColor}Press any key to continue" + ); + }); +} + +#[test] +fn handle_process_result_external_command_executable_not_found() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut modules = create_modules(); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let command = String::from( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test") + .join("not-found.sh") + .to_str() + .unwrap(), + ); + let result = ProcessResult::new().external_command(command.clone(), vec![]); + process.handle_process_result(&mut modules, &result); + assert_eq!(process.state, State::Error); + let view_data = modules.build_view_data(process.state, &process.render_context, &process.rebase_todo); + assert_rendered_output!( + view_data, + "{TITLE}", + "{BODY}", + format!("{{Normal}}Unable to run {} ", command), + if cfg!(windows) { + "{Normal}The system cannot find the file specified. (os error 2)" + } + else { + "{Normal}No such file or directory (os error 2)" + }, + "{TRAILING}", + "{IndicatorColor}Press any key to continue" + ); + }); +} + +#[test] +fn handle_process_result_external_command_status_success() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut modules = create_modules(); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let command = String::from("true"); + let result = ProcessResult::new().external_command(command, vec![]); + process.handle_process_result(&mut modules, &result); + assert_eq!( + process.event_handler.read_event(&InputOptions::new(), |e, _| e), + Event::from(MetaEvent::ExternalCommandSuccess) + ); + }); +} + +#[test] +fn handle_process_result_external_command_status_error() { + module_test(&["pick aaa comment"], &[], |test_context| { + let crossterm = create_crossterm(); + let display = Display::new(crossterm, &create_theme()); + let view = View::new(display, "~", "?"); + let mut modules = create_modules(); + let mut process = Process::new( + test_context.rebase_todo_file, + test_context.event_handler_context.event_handler, + view, + ); + let command = String::from("false"); + let result = ProcessResult::new().external_command(command, vec![]); + process.handle_process_result(&mut modules, &result); + assert_eq!( + process.event_handler.read_event(&InputOptions::new(), |e, _| e), + Event::from(MetaEvent::ExternalCommandError) + ); + }); +} |