diff --git a/Cargo.lock b/Cargo.lock index cd66d67..d95e370 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,16 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -665,6 +675,7 @@ dependencies = [ "common", "crossterm", "futures", + "mockito", "num-modular", "ratatui", "reqwest", @@ -713,6 +724,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "common" version = "0.1.0" @@ -1719,6 +1740,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mockito" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6e023aa5bdf392aa06c78e4a4e6d498baab5138d0c993503350ebbc37bf1e" +dependencies = [ + "assert-json-diff", + "colored", + "futures-core", + "hyper", + "log", + "rand", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -2816,6 +2856,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "similar" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" + [[package]] name = "slab" version = "0.4.9" diff --git a/client/Cargo.toml b/client/Cargo.toml index c67ee06..997fd50 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -33,4 +33,7 @@ reqwest-middleware = "0.2.5" [profile.dev.package.backtrace] opt-level = 3 +[dev-dependencies] +mockito = "1.4.0" + diff --git a/client/src/main.rs b/client/src/main.rs index 57f9b30..0a904e4 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,10 +1,10 @@ //! Client - +#![feature(coverage_attribute)] #![warn(rustdoc::private_doc_tests)] #![warn(missing_docs)] #![warn(rustdoc::missing_crate_level_docs)] use std::{ - io::{self, Write}, + io::{self, stdout}, panic, }; @@ -28,21 +28,23 @@ const TEXT_COLOR: Color = Color::White; const SELECTED_STYLE_FG: Color = Color::LightYellow; const COMPLETED_TEXT_COLOR: Color = Color::Green; -#[tokio::main] -async fn main() -> color_eyre::Result<()> { - initialize_logging()?; - install_hooks()?; - let term = term::init(std::io::stdout())?; - let res = run(term).await; - term::restore()?; - res -} -async fn run(mut term: term::Tui) -> color_eyre::Result<()> { - let state = mid::init("http://localhost:8080").await?; - let events = EventStream::new(); - App::new(state).run(&mut term, events).await +#[coverage(off)] +fn main() -> color_eyre::Result<()> { + // manually create tokio runtime + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(#[coverage(off)] async { + initialize_logging()?; + install_hooks()?; + term::enable()?; + let state = mid::init("http://localhost:8080").await?; + let res = run(CrosstermBackend::new(stdout()), state, EventStream::new()).await; + term::restore()?; + res?; + Ok(()) + }) } +#[coverage(off)] fn initialize_logging() -> color_eyre::Result<()> { let file_subscriber = tracing_subscriber::fmt::layer() .with_file(true) @@ -60,21 +62,22 @@ fn initialize_logging() -> color_eyre::Result<()> { /// This replaces the standard color_eyre panic and error hooks with hooks that /// restore the terminal before printing the panic or error. +#[coverage(off)] pub fn install_hooks() -> color_eyre::Result<()> { // add any extra configuration you need to the hook builder let hook_builder = color_eyre::config::HookBuilder::default(); let (panic_hook, eyre_hook) = hook_builder.into_hooks(); - // convert from a color_eyre PanicHook to a standard panic hook + // used color_eyre's PanicHook as the standard panic hook let panic_hook = panic_hook.into_panic_hook(); - panic::set_hook(Box::new(move |panic_info| { + panic::set_hook(Box::new(#[coverage(off)] move |panic_info| { term::restore().unwrap(); panic_hook(panic_info); })); - // convert from a color_eyre EyreHook to a eyre ErrorHook + // use color_eyre's EyreHook as eyre's ErrorHook let eyre_hook = eyre_hook.into_eyre_hook(); - eyre::set_hook(Box::new(move |error| { + eyre::set_hook(Box::new(#[coverage(off)] move |error| { term::restore().unwrap(); eyre_hook(error) }))?; @@ -82,14 +85,23 @@ pub fn install_hooks() -> color_eyre::Result<()> { Ok(()) } +/// Run the program using writer, state, and event stream. abstracts between tests & main +async fn run(backend: B, state: State, events: impl Stream> + Unpin) -> color_eyre::Result { + let mut term = Terminal::new(backend)?; + let mut app = App::new(state); + app.run(&mut term, events).await?; + Ok(app) +} + /// UI App State pub struct App { - /// should exit + /// flag to be set to exit the event loop should_exit: bool, /// middleware state state: State, /// task list widget task_list: TaskList, + /// number of frame updates (used for debug purposes) updates: usize, } @@ -100,6 +112,7 @@ pub struct TaskList { list_state: ListState, } impl TaskList { + // move current selection of task up 1 item. fn up(&mut self, state: &State) { let Some(tasks) = self.current_view.and_then(|vk| state.view_tasks(vk)) else { self.list_state.select(None); @@ -113,6 +126,7 @@ impl TaskList { .map_or(0, |v| v.subm(1, &tasks.len())), )); } + // move current selection of task down 1 item fn down(&mut self, state: &State) { let Some(tasks) = self.current_view.and_then(|vk| state.view_tasks(vk)) else { self.list_state.select(None); @@ -124,6 +138,7 @@ impl TaskList { .map_or(1, |v| v.addm(1, &tasks.len())), )); } + // render task list to buffer fn render(&mut self, state: &State, block: Block<'_>, area: Rect, buf: &mut Buffer) { // take items from the current view and render them into a list if let Some(items) = self @@ -134,6 +149,7 @@ impl TaskList { .iter() .flat_map(|key| { let task = state.task_get(*key)?; + // render task line Some(match task.completed { false => Line::styled(format!(" ☐ {}", task.name), TEXT_COLOR), true => Line::styled(format!(" ✓ {}", task.name), COMPLETED_TEXT_COLOR), @@ -170,7 +186,7 @@ impl TaskList { } impl App { - /// runs the application's main loop until the user quits + /// create new app given middleware state pub fn new(state: State) -> Self { Self { should_exit: false, @@ -180,25 +196,23 @@ impl App { } } /// run app with some terminal output and event stream input - pub async fn run( + pub async fn run( &mut self, - term: &mut term::Tui, + term: &mut term::Tui, mut events: impl Stream> + Unpin, ) -> color_eyre::Result<()> { self.task_list.current_view = self.state.view_get_default(); - // while not exist - while !self.should_exit { - self.updates += 1; // keep track of update & render - term.draw(|frame| frame.render_widget(&mut *self, frame.size()))?; - - // listen for evens and only re-render if we receive one that would imply we need to re-render - let mut do_render = false; - while !do_render { - let Some(event) = events.next().await else { - continue; - }; - do_render = self.handle_event(event?)? + // render initial frame + term.draw(|frame| frame.render_widget(&mut *self, frame.size()))?; + // wait for events + while let Some(event) = events.next().await { + // if we determined that event should trigger redraw: + if self.handle_event(event?)? { + // draw frame + term.draw(|frame| frame.render_widget(&mut *self, frame.size()))?; } + // if we should exit, break loop + if self.should_exit { break } } Ok(()) } @@ -241,6 +255,8 @@ impl App { impl Widget for &mut App { fn render(self, area: Rect, buf: &mut Buffer) { + self.updates += 1; // record render count + let title = Title::from(" Task Management ".bold()); // bottom bar instructions let instructions = Title::from(Line::from(vec![ @@ -251,6 +267,7 @@ impl Widget for &mut App { ", Quit: ".into(), " ".blue().bold(), ])); + // bottom right render update count let update_counter = Title::from(format!("Updates: {}", self.updates)); let block = Block::default() .bg(BACKGROUND) @@ -277,61 +294,91 @@ mod tests { use std::time::Duration; use futures::SinkExt; + use ratatui::backend::TestBackend; use super::*; - #[test] - fn dummy_test_main() { - std::thread::spawn(main); - std::thread::sleep(Duration::from_millis(250)); - term::restore().unwrap(); - } - #[tokio::test] async fn mock_app() { - let out = Box::leak(Box::new(Vec::new())); - let writer = io::BufWriter::new(out); + let backend = TestBackend::new(55, 5); + let (mut sender, events) = futures::channel::mpsc::channel(10); + + let join = tokio::spawn(run(backend, init_test_state().0, events)); + + // test regular event + sender + .send(Ok(Event::Key(KeyCode::Up.into()))) + .await + .unwrap(); + tokio::time::sleep(Duration::from_millis(50)).await; + // test non-rendering event + sender + .send(Ok(Event::Key(KeyCode::Char('1').into()))) + .await + .unwrap(); + tokio::time::sleep(Duration::from_millis(50)).await; + // test error event + sender + .send(Err(io::Error::other::("error".into()))) + .await + .unwrap(); + assert!(join.await.unwrap().is_err()); + + let backend = TestBackend::new(55, 5); let (mut sender, events) = futures::channel::mpsc::channel(10); - let good_event = sender.send(Ok(Event::Key(KeyCode::Up.into()))); - let join = tokio::spawn(async move { - let mut term = term::init(writer).unwrap(); - let mut app = App::new(init_test_state().0); - let res = app.run(&mut term, events).await; - term::restore().unwrap(); - }); - assert!(good_event.await.is_ok()); + let join = tokio::spawn(run(backend, init_test_state().0, events)); + // test resize app + sender + .send(Ok(Event::Resize(0, 0))) + .await + .unwrap(); + tokio::time::sleep(Duration::from_millis(50)).await; + // test quit app + sender + .send(Ok(Event::Key(KeyCode::Char('q').into()))) + .await + .unwrap(); + tokio::time::sleep(Duration::from_millis(50)).await; + assert!(join.await.unwrap().is_ok()); + } #[test] fn render_test() { + // test default state let mut app = App::new(State::default()); let mut buf = Buffer::empty(Rect::new(0, 0, 55, 5)); app.render(buf.area, &mut buf); - + buf.set_style(Rect::new(0, 0, 55, 5), Style::reset()); let expected = Buffer::with_lines(vec![ "╭────────────────── Task Management ──────────────────╮", "│ No Task Views to Display │", "│ │", "│ │", - "╰────────── Select: /, Quit: ─Updates: 0╯", + "╰────────── Select: /, Quit: ─Updates: 1╯", ]); - buf.set_style(Rect::new(0, 0, 50, 7), Style::reset()); - - // don't bother checking styles, they change too frequently - /* - let title_style = Style::new().bold(); - let counter_style = Style::new().yellow(); - let key_style = Style::new().blue().bold(); - expected.set_style(Rect::new(16, 0, 17, 1), title_style); - expected.set_style(Rect::new(28, 1, 1, 1), counter_style); - expected.set_style(Rect::new(13, 3, 6, 1), key_style); - expected.set_style(Rect::new(30, 3, 7, 1), key_style); - expected.set_style(Rect::new(43, 3, 4, 1), key_style); */ // note ratatui also has an assert_buffer_eq! macro that can be used to // compare buffers and display the differences in a more readable way assert_eq!(buf, expected); + + // test task state + let (state, view_key) = init_test_state(); + let mut app = App::new(state); + app.task_list.current_view = Some(view_key); + let mut buf = Buffer::empty(Rect::new(0, 0, 55, 5)); + + app.render(buf.area, &mut buf); + buf.set_style(Rect::new(0, 0, 55, 5), Style::reset()); + let expected = Buffer::with_lines(vec![ + "╭────────────────── Task Management ──────────────────╮", + "│ ✓ Eat Lunch │", + "│ ☐ Finish ABN │", + "│ │", + "╰────────── Select: /, Quit: ─Updates: 1╯", + ]); + assert_eq!(buf, expected); } #[test] diff --git a/client/src/mid.rs b/client/src/mid.rs index 9e08f70..89b7f46 100644 --- a/client/src/mid.rs +++ b/client/src/mid.rs @@ -1,23 +1,20 @@ //! Middleware Logic -#![allow(unused)] - - -use color_eyre::{eyre::Context, Section}; +#![allow(unused)] // for my sanity developing (TODO: remove this later) +use color_eyre::eyre::Context; use common::{ backend::{ - FilterRequest, FilterResponse, ReadTaskShortRequest, ReadTaskShortResponse, + FilterRequest, ReadTaskShortRequest, ReadTasksShortRequest, ReadTasksShortResponse, }, *, }; -use reqwest::{Request, Response}; -use reqwest_middleware::ClientBuilder; +use reqwest::Response; +use reqwest_middleware::{ClientBuilder, RequestBuilder}; use reqwest_tracing::{SpanBackendWithUrl, TracingMiddleware}; use serde::{Deserialize, Serialize}; use slotmap::{new_key_type, SlotMap}; use std::collections::HashMap; use thiserror::Error; -// use api::get_task_request; new_key_type! { pub struct PropKey; } new_key_type! { pub struct TaskKey; } @@ -119,69 +116,29 @@ enum StateEvent { ServerStatus(bool), } -/* /// Frontend API Trait -pub trait FrontendAPI { - - /// create/view/modify tasks - fn task_def(&mut self, task: Task) -> TaskKey; - fn task_get(&self, key: TaskKey) -> Option<&Task>; - fn task_mod(&mut self, key: TaskKey, edit_fn: impl FnOnce(&mut Task)); - fn task_rm(&mut self, key: TaskKey); - - /// create/view/modify task properties - fn prop_def(&mut self, task_key: TaskKey, name_key: PropNameKey, prop: TaskPropVariant) -> Result; - fn prop_def_name(&mut self, name: impl Into) -> PropNameKey; - fn prop_rm_name(&mut self, key: PropNameKey); - fn prop_get(&self, task_key: TaskKey, name: PropNameKey) -> Result<&TaskPropVariant, PropDataError>; - fn prop_mod( - &mut self, - task_key: TaskKey, - name: PropNameKey, - edit_fn: impl FnOnce(&mut TaskPropVariant), - ) -> Result<(), PropDataError>; - fn prop_rm(&mut self, task_key: TaskKey, name: PropNameKey) -> Result; - - /// create/get/modify views - fn view_def(&mut self, view: View) -> ViewKey; - fn view_get(&self, view_key: ViewKey) -> Option<&View>; - fn view_tasks(&self, view_key: ViewKey) -> Option<&[TaskKey]>; - fn view_mod(&mut self, view_key: ViewKey, edit_fn: impl FnOnce(&mut View)) -> Option<()>; - fn view_rm(&mut self, view_key: ViewKey); - - /// create/get/modify script data. - fn script_create(&mut self) -> ScriptID; - fn script_get(&self, script_id: ScriptID) -> Option<&Script>; - fn script_mod(&mut self, script_id: ScriptID, edit_fn: impl FnOnce(&mut Script)); - fn script_rm(&mut self, script_id: ScriptID); - - /// register ui events with middleware, (i.e. so scripts can run when they are triggered) - fn register_event(&mut self, name: &str); - /// notify middleware of registered event - fn event_notify(&mut self, name: &str) -> bool; -} */ - impl State { + /// define a task, get a key that uniquely identifies it pub fn task_def(&mut self, task: Task) -> TaskKey { // TODO: register definition to queue so that we can sync to server self.tasks.insert(task) } - + /// get task using a key, if it exists pub fn task_get(&self, key: TaskKey) -> Option<&Task> { self.tasks.get(key) } - + /// modify a task pub fn task_mod(&mut self, key: TaskKey, edit_fn: impl FnOnce(&mut Task)) { if let Some(task) = self.tasks.get_mut(key) { edit_fn(task) } } - + /// delete a task pub fn task_rm(&mut self, key: TaskKey) { if let Some(db_id) = self.tasks.remove(key).and_then(|t| t.db_id) { self.task_map.remove(&db_id); } } - + /// define a property of a certain type on an associated task pub fn prop_def( &mut self, task_key: TaskKey, @@ -199,12 +156,14 @@ impl State { self.prop_map.insert((task_key, name_key), prop_key); Ok(prop_key) } + /// define a property name pub fn prop_def_name(&mut self, name: impl Into) -> PropNameKey { let name: String = name.into(); let key = self.prop_names.insert(name.clone()); self.prop_name_map.insert(name, key); key } + /// delete a property name pub fn prop_rm_name(&mut self, name_key: PropNameKey) -> Result { let name = self .prop_names @@ -213,7 +172,7 @@ impl State { self.prop_name_map.remove(&name); Ok(name) } - + /// get a property pub fn prop_get( &self, task_key: TaskKey, @@ -232,7 +191,7 @@ impl State { .ok_or(PropDataError::Prop(task_key, name_key))?; Ok(&self.props[*key]) } - + /// modify a property pub fn prop_mod( &mut self, task_key: TaskKey, @@ -257,7 +216,7 @@ impl State { ); Ok(()) } - + /// delete a property pub fn prop_rm( &mut self, task_key: TaskKey, @@ -278,48 +237,49 @@ impl State { .remove(key) .ok_or(PropDataError::Prop(task_key, name_key)) } - + /// define a view pub fn view_def(&mut self, view: View) -> ViewKey { // TODO: register to save updated view self.views.insert(view) } - + /// get a view pub fn view_get(&self, view_key: ViewKey) -> Option<&View> { self.views.get(view_key) } + /// get the default view pub fn view_get_default(&self) -> Option { self.views.keys().next() } - + /// shorthand function to get the list of tasks associated with a view pub fn view_tasks(&self, view_key: ViewKey) -> Option<&[TaskKey]> { self.views .get(view_key) .and_then(|v| v.tasks.as_ref()) .map(|v| v.as_slice()) } - + /// modify a view pub fn view_mod(&mut self, view_key: ViewKey, edit_fn: impl FnOnce(&mut View)) -> Option<()> { edit_fn(self.views.get_mut(view_key)?); None } - + /// delete a view pub fn view_rm(&mut self, view_key: ViewKey) { self.views.remove(view_key); } - + /// create a script pub fn script_create(&mut self) -> ScriptID { self.scripts.insert(0, Script::default()); 0 } - + /// get a script pub fn script_get(&self, script_id: ScriptID) -> Option<&Script> { self.scripts.get(&script_id) } - + /// modify a script pub fn script_mod(&mut self, script_id: ScriptID, edit_fn: impl FnOnce(&mut Script)) { self.scripts.entry(script_id).and_modify(edit_fn); } - + /// delete a script pub fn script_rm(&mut self, script_id: ScriptID) { self.scripts.remove(&script_id); } @@ -333,6 +293,26 @@ impl State { } */ } +// request helper function +#[tracing::instrument] +async fn do_request(req_builder: RequestBuilder, req: Req) -> color_eyre::Result + where Req: Serialize + std::fmt::Debug, Res: for<'d> Deserialize<'d> + std::fmt::Debug +{ + let res: Response = req_builder + .json(&req) + .send() + .await + .with_context(|| "failed to send request to {url}")?; + let bytes = res.bytes().await?.to_vec(); + let res: Res = serde_json::from_reader(&bytes[..]).with_context(|| { + format!( + "should have received type {}, as json, received: \"{}\"", + std::any::type_name::(), String::from_utf8_lossy(&bytes) + ) + })?; + Ok(res) +} + /// Init middleware state /// This function is called by UI to create the Middleware state and establish a connection to the Database. /// Important: Make sure `url` does not contain a trailing `/` @@ -346,74 +326,43 @@ pub async fn init(url: &str) -> color_eyre::Result { let client = ClientBuilder::new(reqwest::Client::new()) .with(TracingMiddleware::::new()) .build(); - // let request = FilterRequest { - // filter: Filter::None, - // }; - // let res: Response = client - // .get(format!("{url}/filter")) - // .json(&request) - // .send() - // .await - // .with_context(|| "sending /filter request")?; - let request = ReadTaskShortRequest { - task_id: 1, - }; - let res = client - .get(format!("{url}/task")) - .json(&request) - .send() - .await?; - - let string = String::from_utf8(res.bytes().await?.to_vec())?; - let res: ReadTaskShortResponse = serde_json::from_str(&string).with_context(|| { - format!( - "received FilterResponse, attempting to deserialize the following as json: \"{}\"", - string.clone() - ) - })?; - - let task_key = Task { - name: res.name, - dependencies: res.deps, - completed: res.completed, - scripts: res.scripts, - db_id: Some(res.task_id), - }; - - state.tasks.insert(task_key); - - let request = ReadTaskShortRequest { - task_id: 2, - }; - let res = client - .get(format!("{url}/task")) - .json(&request) - .send() - .await?; - - let string = String::from_utf8(res.bytes().await?.to_vec())?; - let res: ReadTaskShortResponse = serde_json::from_str(&string).with_context(|| { - format!( - "received FilterResponse, attempting to deserialize the following as json: \"{}\"", - string.clone() - ) - })?; - let task_key = Task { - name: res.name, - dependencies: res.deps, - completed: res.completed, - scripts: res.scripts, - db_id: Some(res.task_id), - }; - - state.tasks.insert(task_key); + // request all tasks using a "None" filter + let filter_request = FilterRequest { filter: Filter::None }; + let task_ids: Vec = do_request(client.get(format!("{url}/filter")), filter_request).await?; + + // request task data for all filter data passed back + let tasks_request = task_ids + .into_iter() + .map(|task_id| ReadTaskShortRequest { task_id }) + .collect::(); + let tasks_res: ReadTasksShortResponse = do_request(client.get(format!("{url}/tasks")), tasks_request).await?; + + // insert received tasks into middleware tasks list + let task_keys = tasks_res.into_iter().flat_map(|res| { + res.ok().map(|res| { + ( + res.task_id, + state.tasks.insert(Task { + name: res.name, + dependencies: res.deps, + completed: res.completed, + scripts: res.scripts, + db_id: Some(res.task_id), + }), + ) + }) + }); + state.task_map.extend(task_keys); // update DbID -> TaskKey map + // create default "Main View" and make it show all default tasks let view_key = state.view_def(View { name: "Main View".to_string(), tasks: Some(state.tasks.keys().collect::>()), ..View::default() }); + let view_tasks = state.tasks.keys().collect::>(); + state.view_mod(view_key, |v| v.tasks = Some(view_tasks)); Ok(state) } @@ -439,10 +388,74 @@ pub fn init_test_state() -> (State, ViewKey) { #[cfg(test)] mod tests { pub use super::*; + use common::backend::{FilterResponse, ReadTaskShortResponse}; + use mockito::Server; + use serde_json::to_vec; + // use tracing_test::traced_test; + + #[tokio::test] + async fn test_do_request() { + + let mut server = Server::new_async().await; + server.mock("GET", mockito::Matcher::Any).with_body("TEST MAIN PATH") + .expect(0) + .create_async().await; + + server.mock("GET", "/shouldincomplete") + .with_body("invalid json") + .expect(1) + .create_async().await; + + // create client + let client = ClientBuilder::new(reqwest::Client::new()) + .with(TracingMiddleware::::new()) + .build(); + + // test can't connect err + do_request::<_, FilterResponse>(client.get("localhost:1234/cantconnect"), FilterRequest{filter:Filter::None}).await.unwrap_err(); + // test can't parse response err + do_request::<_, FilterResponse>(client.get(format!("{}/shouldincomplete", server.url())), FilterRequest{filter:Filter::None}).await.unwrap_err(); + } #[tokio::test] + // #[traced_test] async fn test_init() { - assert!(init("http:localhost:69420").await.is_err()) + let mut server = Server::new_async().await; + + server.mock("GET", "/filter") + //.match_body(Matcher::Json(to_value(FilterRequest { filter: Filter::None }).unwrap())) + .with_body(to_vec(&vec![0, 1, 2]).unwrap()) + .expect(1) + .create_async().await; + + server.mock("GET", "/tasks") + //.match_body(Matcher::Json(to_value(&vec![0, 1, 2].into_iter().map(|task_id|ReadTaskShortRequest{task_id}).collect::>()).unwrap())) + .with_body(&to_vec::(&vec![ + Ok(ReadTaskShortResponse { task_id: 0, name: "Test Task 1".into(), ..Default::default() }), + Ok(ReadTaskShortResponse { task_id: 1, name: "Test Task 2".into(), ..Default::default() }), + Err("random error message".into()), + Ok(ReadTaskShortResponse { task_id: 2, name: "Test Task 3".into(), ..Default::default() }), + ]).unwrap()) + .expect(1) + .create_async().await; + + server.mock("GET", mockito::Matcher::Any).with_body("TEST MAIN PATH") + .expect(0) + .create_async().await; + + let url = server.url(); + println!("url: {url}"); + + // init state + let state = init(&url).await.unwrap(); + + // make sure view was created with correct state + let view = state.view_get(state.view_get_default().unwrap()).unwrap(); + let mut i = 0; + view.tasks.as_ref().unwrap().iter().for_each(|t|{ + assert_eq!(state.task_get(*t).unwrap().db_id.unwrap(), i); + i+=1; + }); } #[test] @@ -464,7 +477,12 @@ mod tests { state.task_map.insert(0, tasks[1]); state.tasks[tasks[1]].db_id = Some(0); state.task_rm(tasks[1]); + // test get function fail assert!(state.task_get(tasks[1]).is_none()); + // test mod function fail + let mut test = 0; + state.task_mod(tasks[1], |_|test = 1); + assert_eq!(test, 0); assert!(state.task_map.is_empty()); let name_key = state.prop_def_name("Due Date"); @@ -474,7 +492,7 @@ mod tests { assert!(state.prop_rm_name(invalid_name_key).is_err()); // test prop_def - state.prop_def(tasks[0], name_key, TaskPropVariant::Boolean(false)); + state.prop_def(tasks[0], name_key, TaskPropVariant::Boolean(false)).unwrap(); assert!(state .prop_def(tasks[0], invalid_name_key, TaskPropVariant::Boolean(false)) .is_err()); @@ -510,7 +528,7 @@ mod tests { ); assert!(state.prop_rm(tasks[0], name_key).is_err()); - /// script testing + // script testing let script_id = state.script_create(); state.script_mod(script_id, |s| s.content = "function do_lua()".to_owned()); assert_eq!( @@ -520,7 +538,7 @@ mod tests { state.script_rm(script_id); assert!(state.script_get(script_id).is_none()); - /// test remove view + // test remove view state.view_rm(view_key); assert!(state.view_get(view_key).is_none()); diff --git a/client/src/term.rs b/client/src/term.rs index c4e00e6..11de4cc 100644 --- a/client/src/term.rs +++ b/client/src/term.rs @@ -1,33 +1,40 @@ -use std::io::{self, stdout, Write}; +//! Terminal module +//! call enable() and restore() for real terminals +//! call create(writer: W) to create the crossterm backend + +use std::io::{self, stdout}; use crossterm::{execute, terminal::*}; use ratatui::prelude::*; /// A type alias for the terminal type used in this application -pub type Tui = Terminal>; +pub type Tui = Terminal; -pub fn init(mut writer: W) -> io::Result> { - execute!(writer, EnterAlternateScreen)?; - enable_raw_mode()?; - Terminal::new(CrosstermBackend::new(writer)) +/// Enter alternate screen (required to initialize terminal) +#[coverage(off)] +pub fn enable() -> io::Result<()> { + execute!(stdout(), EnterAlternateScreen)?; + enable_raw_mode() } -/// Restore the terminal to its original state +/// Leave alternate screen (cleanup crossterm) +#[coverage(off)] pub fn restore() -> io::Result<()> { execute!(stdout(), LeaveAlternateScreen)?; - disable_raw_mode()?; - Ok(()) + disable_raw_mode() } -#[cfg(test)] +/* #[cfg(test)] mod tests { use super::*; #[test] fn terminal_wrap() { - let mut out = Vec::::new(); - let _ = init::<&mut Vec>(&mut out); - - let _ = restore(); + if std::env::var("TERM").is_ok() { return; } // This test does not work on CI + // disable if running on github actions + if std::env::var("GITHUB_ACTIONS").is_ok() { return; } + + let _ = enable().unwrap(); + let _ = restore().unwrap(); } -} +} */ diff --git a/common/src/backend.rs b/common/src/backend.rs index c6ec79a..655b8cb 100644 --- a/common/src/backend.rs +++ b/common/src/backend.rs @@ -26,13 +26,13 @@ pub type CreateTasksRequest = Vec; pub type CreateTasksResponse = Vec; /// reawest::get("/task") -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct ReadTaskShortRequest { /// task id to request pub task_id: TaskID, } /// response to GET /task -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Default)] pub struct ReadTaskShortResponse { /// task id of response, should be the same as request pub task_id: TaskID, @@ -97,7 +97,7 @@ type PropertiesResponse = Vec<(String, Vec)>; /// # FILTER APIS /// reqwest::get("/filter") -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct FilterRequest { /// filter to apply pub filter: Filter, diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..25357f6 --- /dev/null +++ b/flake.nix @@ -0,0 +1,32 @@ +{ + description = "Rust Devshell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { nixpkgs, rust-overlay, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + in + with pkgs; + { + devShells.default = mkShell { + buildInputs = [ + openssl + pkg-config + (rust-bin.fromRustupToolchainFile ./rust-toolchain.toml) + cargo-llvm-cov + cargo-edit + lcov + ]; + }; + } + ); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 0158d7f..711c87a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,10 +1,9 @@ [toolchain] -channel = "stable" # or whatever channel you are using +channel = "nightly-2024-03-20" # or whatever channel you are using components = [ "rust-src", "rust-analyzer", "clippy", "rustfmt", "llvm-tools-preview", - "cargo-llvm-cov", ]