Merge pull request #71 from djmitche/issue7

Add start and stop commands
This commit is contained in:
Dustin J. Mitchell 2020-11-28 23:19:21 -05:00 committed by GitHub
commit 47cd051bd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 360 additions and 9 deletions

57
cli/src/cmd/debug.rs Normal file
View file

@ -0,0 +1,57 @@
use crate::table;
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
use failure::Fallible;
use prettytable::{cell, row, Table};
use crate::cmd::{shared, ArgMatchResult, CommandInvocation};
#[derive(Debug)]
struct Invocation {
task: String,
}
define_subcommand! {
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
app.subcommand(
ClapSubCommand::with_name("debug")
.about("debug info for the given task")
.arg(shared::task_arg()))
}
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
match matches.subcommand() {
("debug", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
task: matches.value_of("task").unwrap().into(),
})),
_ => ArgMatchResult::None,
}
}
}
subcommand_invocation! {
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
let mut replica = command.get_replica();
let task = shared::get_task(&mut replica, &self.task)?;
let mut t = Table::new();
t.set_format(table::format());
t.set_titles(row![b->"key", b->"value"]);
for (k, v) in task.get_taskmap().iter() {
t.add_row(row![k, v]);
}
t.printstd();
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_command() {
with_subcommand_invocation!(vec!["task", "debug", "1"], |inv: &Invocation| {
assert_eq!(inv.task, "1".to_string());
});
}
}

View file

@ -42,6 +42,7 @@ subcommand_invocation! {
} }
t.add_row(row![b->"Description", task.get_description()]); t.add_row(row![b->"Description", task.get_description()]);
t.add_row(row![b->"Status", task.get_status()]); t.add_row(row![b->"Status", task.get_status()]);
t.add_row(row![b->"Active", task.is_active()]);
t.printstd(); t.printstd();
Ok(()) Ok(())
} }

View file

@ -26,13 +26,17 @@ subcommand_invocation! {
let mut replica = command.get_replica()?; let mut replica = command.get_replica()?;
let mut t = Table::new(); let mut t = Table::new();
t.set_format(table::format()); t.set_format(table::format());
t.set_titles(row![b->"id", b->"description"]); t.set_titles(row![b->"id", b->"act", b->"description"]);
for (uuid, task) in replica.all_tasks().unwrap() { for (uuid, task) in replica.all_tasks().unwrap() {
let mut id = uuid.to_string(); let mut id = uuid.to_string();
if let Some(i) = replica.get_working_set_index(&uuid)? { if let Some(i) = replica.get_working_set_index(&uuid)? {
id = i.to_string(); id = i.to_string();
} }
t.add_row(row![id, task.get_description()]); let active = match task.is_active() {
true => "*",
false => "",
};
t.add_row(row![id, active, task.get_description()]);
} }
t.printstd(); t.printstd();
Ok(()) Ok(())

View file

@ -6,22 +6,28 @@ mod macros;
mod shared; mod shared;
mod add; mod add;
mod debug;
mod gc; mod gc;
mod info; mod info;
mod list; mod list;
mod modify; mod modify;
mod pending; mod pending;
mod start;
mod stop;
mod sync; mod sync;
/// Get a list of all subcommands in this crate /// Get a list of all subcommands in this crate
pub(crate) fn subcommands() -> Vec<Box<dyn SubCommand>> { pub(crate) fn subcommands() -> Vec<Box<dyn SubCommand>> {
vec![ vec![
add::cmd(), add::cmd(),
debug::cmd(),
gc::cmd(), gc::cmd(),
info::cmd(), info::cmd(),
list::cmd(), list::cmd(),
modify::cmd(), modify::cmd(),
pending::cmd(), pending::cmd(),
start::cmd(),
stop::cmd(),
sync::cmd(), sync::cmd(),
] ]
} }

View file

@ -28,10 +28,14 @@ subcommand_invocation! {
let working_set = command.get_replica()?.working_set().unwrap(); let working_set = command.get_replica()?.working_set().unwrap();
let mut t = Table::new(); let mut t = Table::new();
t.set_format(table::format()); t.set_format(table::format());
t.set_titles(row![b->"id", b->"description"]); t.set_titles(row![b->"id", b->"act", b->"description"]);
for (i, item) in working_set.iter().enumerate() { for (i, item) in working_set.iter().enumerate() {
if let Some(ref task) = item { if let Some(ref task) = item {
t.add_row(row![i, task.get_description()]); let active = match task.is_active() {
true => "*",
false => "",
};
t.add_row(row![i, active, task.get_description()]);
} }
} }
t.printstd(); t.printstd();

48
cli/src/cmd/start.rs Normal file
View file

@ -0,0 +1,48 @@
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
use failure::Fallible;
use crate::cmd::{shared, ArgMatchResult, CommandInvocation};
#[derive(Debug)]
struct Invocation {
task: String,
}
define_subcommand! {
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
app.subcommand(
ClapSubCommand::with_name("start")
.about("start the given task")
.arg(shared::task_arg()))
}
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
match matches.subcommand() {
("start", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
task: matches.value_of("task").unwrap().into(),
})),
_ => ArgMatchResult::None,
}
}
}
subcommand_invocation! {
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
let mut replica = command.get_replica();
let task = shared::get_task(&mut replica, &self.task)?;
task.into_mut(&mut replica).start()?;
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_command() {
with_subcommand_invocation!(vec!["task", "start", "1"], |inv: &Invocation| {
assert_eq!(inv.task, "1".to_string());
});
}
}

48
cli/src/cmd/stop.rs Normal file
View file

@ -0,0 +1,48 @@
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
use failure::Fallible;
use crate::cmd::{shared, ArgMatchResult, CommandInvocation};
#[derive(Debug)]
struct Invocation {
task: String,
}
define_subcommand! {
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
app.subcommand(
ClapSubCommand::with_name("stop")
.about("stop the given task")
.arg(shared::task_arg()))
}
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
match matches.subcommand() {
("stop", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
task: matches.value_of("task").unwrap().into(),
})),
_ => ArgMatchResult::None,
}
}
}
subcommand_invocation! {
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
let mut replica = command.get_replica();
let task = shared::get_task(&mut replica, &self.task)?;
task.into_mut(&mut replica).stop()?;
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_command() {
with_subcommand_invocation!(vec!["task", "stop", "1"], |inv: &Invocation| {
assert_eq!(inv.task, "1".to_string());
});
}
}

View file

@ -30,6 +30,7 @@ The following keys, and key formats, are defined:
* `status` - one of `P` for a pending task (the default), `C` for completed or `D` for deleted * `status` - one of `P` for a pending task (the default), `C` for completed or `D` for deleted
* `description` - the one-line summary of the task * `description` - the one-line summary of the task
* `modified` - the time of the last modification of this task * `modified` - the time of the last modification of this task
* `start.<timestamp>` - either an empty string (representing work on the task to the task that has not been stopped) or a timestamp (representing the time that work stopped)
The following are not yet implemented: The following are not yet implemented:

View file

@ -118,6 +118,10 @@ impl Task {
&self.uuid &self.uuid
} }
pub fn get_taskmap(&self) -> &TaskMap {
&self.taskmap
}
/// Prepare to mutate this task, requiring a mutable Replica /// Prepare to mutate this task, requiring a mutable Replica
/// in order to update the data it contains. /// in order to update the data it contains.
pub fn into_mut(self, replica: &mut Replica) -> TaskMut { pub fn into_mut(self, replica: &mut Replica) -> TaskMut {
@ -142,6 +146,16 @@ impl Task {
.unwrap_or("") .unwrap_or("")
} }
/// Determine whether this task is active -- that is, that it has been started
/// and not stopped.
pub fn is_active(&self) -> bool {
self.taskmap
.iter()
.filter(|(k, v)| k.starts_with("start.") && v.is_empty())
.next()
.is_some()
}
pub fn get_modified(&self) -> Option<DateTime<Utc>> { pub fn get_modified(&self) -> Option<DateTime<Utc>> {
self.get_timestamp("modified") self.get_timestamp("modified")
} }
@ -184,6 +198,33 @@ impl<'r> TaskMut<'r> {
self.set_timestamp("modified", Some(modified)) self.set_timestamp("modified", Some(modified))
} }
/// Start the task by creating "start.<timestamp": "", if the task is not already
/// active.
pub fn start(&mut self) -> Fallible<()> {
if self.is_active() {
return Ok(());
}
let k = format!("start.{}", Utc::now().timestamp());
self.set_string(k.as_ref(), Some(String::from("")))
}
/// Stop the task by adding the current timestamp to all un-resolved "start.<timestamp>" keys.
pub fn stop(&mut self) -> Fallible<()> {
let keys = self
.taskmap
.iter()
.filter(|(k, v)| k.starts_with("start.") && v.is_empty())
.map(|(k, _)| k)
.cloned()
.collect::<Vec<_>>();
let now = Utc::now();
for key in keys {
println!("{}", key);
self.set_timestamp(&key, Some(now))?;
}
Ok(())
}
// -- utility functions // -- utility functions
fn lastmod(&mut self) -> Fallible<()> { fn lastmod(&mut self) -> Fallible<()> {
@ -212,11 +253,26 @@ impl<'r> TaskMut<'r> {
fn set_timestamp(&mut self, property: &str, value: Option<DateTime<Utc>>) -> Fallible<()> { fn set_timestamp(&mut self, property: &str, value: Option<DateTime<Utc>>) -> Fallible<()> {
self.lastmod()?; self.lastmod()?;
self.replica.update_task( if let Some(value) = value {
self.task.uuid, let ts = format!("{}", value.timestamp());
property, self.replica
value.map(|v| format!("{}", v.timestamp())), .update_task(self.task.uuid, property, Some(ts.clone()))?;
) self.task.taskmap.insert(property.to_string(), ts);
} else {
self.replica
.update_task::<_, &str>(self.task.uuid, property, None)?;
self.task.taskmap.remove(property);
}
Ok(())
}
/// Used by tests to ensure that updates are properly written
#[cfg(test)]
fn reload(&mut self) -> Fallible<()> {
let uuid = self.uuid;
let task = self.replica.get_task(&uuid)?.unwrap();
self.task.taskmap = task.taskmap;
Ok(())
} }
} }
@ -232,6 +288,132 @@ impl<'r> std::ops::Deref for TaskMut<'r> {
mod test { mod test {
use super::*; use super::*;
fn with_mut_task<F: FnOnce(TaskMut)>(f: F) {
let mut replica = Replica::new_inmemory();
let task = replica.new_task(Status::Pending, "test".into()).unwrap();
let task = task.into_mut(&mut replica);
f(task)
}
#[test]
fn test_is_active_never_started() {
let task = Task::new(Uuid::new_v4(), TaskMap::new());
assert!(!task.is_active());
}
#[test]
fn test_is_active() {
let task = Task::new(
Uuid::new_v4(),
vec![(String::from("start.1234"), String::from(""))]
.drain(..)
.collect(),
);
assert!(task.is_active());
}
#[test]
fn test_is_active_stopped() {
let task = Task::new(
Uuid::new_v4(),
vec![(String::from("start.1234"), String::from("1235"))]
.drain(..)
.collect(),
);
assert!(!task.is_active());
}
fn count_taskmap(task: &TaskMut, f: fn(&(&String, &String)) -> bool) -> usize {
task.taskmap.iter().filter(f).count()
}
#[test]
fn test_start() {
with_mut_task(|mut task| {
task.start().unwrap();
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
1
);
task.reload().unwrap();
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
1
);
// second start doesn't change anything..
task.start().unwrap();
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
1
);
task.reload().unwrap();
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
1
);
});
}
#[test]
fn test_stop() {
with_mut_task(|mut task| {
task.start().unwrap();
task.stop().unwrap();
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
0
);
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && !v.is_empty()),
1
);
task.reload().unwrap();
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
0
);
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && !v.is_empty()),
1
);
});
}
#[test]
fn test_stop_multiple() {
with_mut_task(|mut task| {
// simulate a task that has (through the synchronization process) been started twice
task.task
.taskmap
.insert(String::from("start.1234"), String::from(""));
task.task
.taskmap
.insert(String::from("start.5678"), String::from(""));
task.stop().unwrap();
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
0
);
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && !v.is_empty()),
2
);
task.reload().unwrap();
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
0
);
assert_eq!(
count_taskmap(&task, |(k, v)| k.starts_with("start.") && !v.is_empty()),
2
);
});
}
#[test] #[test]
fn test_priority() { fn test_priority() {
assert_eq!(Priority::L.to_taskmap(), "L"); assert_eq!(Priority::L.to_taskmap(), "L");