refactor taskchampion::task into submodules
This commit is contained in:
10
taskchampion/src/task/annotation.rs
Normal file
10
taskchampion/src/task/annotation.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use super::Timestamp;
|
||||
|
||||
/// An annotation for a task
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Annotation {
|
||||
/// Time the annotation was made
|
||||
pub entry: Timestamp,
|
||||
/// Content of the annotation
|
||||
pub description: String,
|
||||
}
|
||||
18
taskchampion/src/task/mod.rs
Normal file
18
taskchampion/src/task/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
#![allow(clippy::module_inception)]
|
||||
use chrono::prelude::*;
|
||||
|
||||
mod annotation;
|
||||
mod priority;
|
||||
mod status;
|
||||
mod tag;
|
||||
mod task;
|
||||
|
||||
pub use annotation::Annotation;
|
||||
pub use priority::Priority;
|
||||
pub use status::Status;
|
||||
pub use tag::{Tag, INVALID_TAG_CHARACTERS};
|
||||
pub use task::{Task, TaskMut};
|
||||
|
||||
use tag::SyntheticTag;
|
||||
|
||||
pub type Timestamp = DateTime<Utc>;
|
||||
48
taskchampion/src/task/priority.rs
Normal file
48
taskchampion/src/task/priority.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
/// The priority of a task
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Priority {
|
||||
/// Low
|
||||
L,
|
||||
/// Medium
|
||||
M,
|
||||
/// High
|
||||
H,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Priority {
|
||||
/// Get a Priority from the 1-character value in a TaskMap,
|
||||
/// defaulting to M
|
||||
pub(crate) fn from_taskmap(s: &str) -> Priority {
|
||||
match s {
|
||||
"L" => Priority::L,
|
||||
"M" => Priority::M,
|
||||
"H" => Priority::H,
|
||||
_ => Priority::M,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the 1-character value for this priority to use in the TaskMap.
|
||||
pub(crate) fn to_taskmap(&self) -> &str {
|
||||
match self {
|
||||
Priority::L => "L",
|
||||
Priority::M => "M",
|
||||
Priority::H => "H",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_priority() {
|
||||
assert_eq!(Priority::L.to_taskmap(), "L");
|
||||
assert_eq!(Priority::M.to_taskmap(), "M");
|
||||
assert_eq!(Priority::H.to_taskmap(), "H");
|
||||
assert_eq!(Priority::from_taskmap("L"), Priority::L);
|
||||
assert_eq!(Priority::from_taskmap("M"), Priority::M);
|
||||
assert_eq!(Priority::from_taskmap("H"), Priority::H);
|
||||
}
|
||||
}
|
||||
54
taskchampion/src/task/status.rs
Normal file
54
taskchampion/src/task/status.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
/// The status of a task. The default status in "Pending".
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Status {
|
||||
Pending,
|
||||
Completed,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
/// Get a Status from the 1-character value in a TaskMap,
|
||||
/// defaulting to Pending
|
||||
pub(crate) fn from_taskmap(s: &str) -> Status {
|
||||
match s {
|
||||
"P" => Status::Pending,
|
||||
"C" => Status::Completed,
|
||||
"D" => Status::Deleted,
|
||||
_ => Status::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the 1-character value for this status to use in the TaskMap.
|
||||
pub(crate) fn to_taskmap(&self) -> &str {
|
||||
match self {
|
||||
Status::Pending => "P",
|
||||
Status::Completed => "C",
|
||||
Status::Deleted => "D",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the full-name value for this status to use in the TaskMap.
|
||||
pub fn to_string(&self) -> &str {
|
||||
// TODO: should be impl Display
|
||||
match self {
|
||||
Status::Pending => "Pending",
|
||||
Status::Completed => "Completed",
|
||||
Status::Deleted => "Deleted",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_status() {
|
||||
assert_eq!(Status::Pending.to_taskmap(), "P");
|
||||
assert_eq!(Status::Completed.to_taskmap(), "C");
|
||||
assert_eq!(Status::Deleted.to_taskmap(), "D");
|
||||
assert_eq!(Status::from_taskmap("P"), Status::Pending);
|
||||
assert_eq!(Status::from_taskmap("C"), Status::Completed);
|
||||
assert_eq!(Status::from_taskmap("D"), Status::Deleted);
|
||||
}
|
||||
}
|
||||
136
taskchampion/src/task/tag.rs
Normal file
136
taskchampion/src/task/tag.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// A Tag is a descriptor for a task, that is either present or absent, and can be used for
|
||||
/// filtering. Tags composed of all uppercase letters are reserved for synthetic tags.
|
||||
///
|
||||
/// Valid tags must not contain whitespace or any of the characters in [`INVALID_TAG_CHARACTERS`].
|
||||
/// The first characters additionally cannot be a digit, and subsequent characters cannot be `:`.
|
||||
/// This definition is based on [that of
|
||||
/// TaskWarrior](https://github.com/GothenburgBitFactory/taskwarrior/blob/663c6575ceca5bd0135ae884879339dac89d3142/src/Lexer.cpp#L146-L164).
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub enum Tag {
|
||||
User(String),
|
||||
Synthetic(SyntheticTag),
|
||||
}
|
||||
|
||||
pub const INVALID_TAG_CHARACTERS: &str = "+-*/(<>^! %=~";
|
||||
|
||||
impl Tag {
|
||||
fn from_str(value: &str) -> Result<Tag, anyhow::Error> {
|
||||
fn err(value: &str) -> Result<Tag, anyhow::Error> {
|
||||
anyhow::bail!("invalid tag {:?}", value)
|
||||
}
|
||||
|
||||
// first, look for synthetic tags
|
||||
if value.chars().all(|c| c.is_ascii_uppercase()) {
|
||||
if let Ok(st) = SyntheticTag::from_str(value) {
|
||||
return Ok(Self::Synthetic(st));
|
||||
}
|
||||
// all uppercase, but not a valid synthetic tag
|
||||
return err(value);
|
||||
}
|
||||
|
||||
if let Some(c) = value.chars().next() {
|
||||
if c.is_whitespace() || c.is_ascii_digit() || INVALID_TAG_CHARACTERS.contains(c) {
|
||||
return err(value);
|
||||
}
|
||||
} else {
|
||||
return err(value);
|
||||
}
|
||||
if !value
|
||||
.chars()
|
||||
.skip(1)
|
||||
.all(|c| !(c.is_whitespace() || c == ':' || INVALID_TAG_CHARACTERS.contains(c)))
|
||||
{
|
||||
return err(value);
|
||||
}
|
||||
Ok(Self::User(String::from(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Tag {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Tag, Self::Error> {
|
||||
Self::from_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&String> for Tag {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &String) -> Result<Tag, Self::Error> {
|
||||
Self::from_str(&value[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Tag {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::User(s) => s.fmt(f),
|
||||
Self::Synthetic(st) => st.as_ref().fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Tag {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::User(s) => s.as_ref(),
|
||||
Self::Synthetic(st) => st.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Hash,
|
||||
strum_macros::EnumString,
|
||||
strum_macros::AsRefStr,
|
||||
strum_macros::EnumIter,
|
||||
)]
|
||||
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum SyntheticTag {
|
||||
Waiting,
|
||||
Active,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[rstest]
|
||||
#[case::simple("abc")]
|
||||
#[case::colon_prefix(":abc")]
|
||||
#[case::letters_and_numbers("a123_456")]
|
||||
#[case::synthetic("WAITING")]
|
||||
fn test_tag_try_into_success(#[case] s: &'static str) {
|
||||
let tag: Tag = s.try_into().unwrap();
|
||||
// check Display (via to_string) and AsRef while we're here
|
||||
assert_eq!(tag.to_string(), s.to_owned());
|
||||
assert_eq!(tag.as_ref(), s);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::empty("")]
|
||||
#[case::colon_infix("a:b")]
|
||||
#[case::digits("999")]
|
||||
#[case::bangs("abc!!!")]
|
||||
#[case::no_such_synthetic("NOSUCH")]
|
||||
fn test_tag_try_into_err(#[case] s: &'static str) {
|
||||
let tag: Result<Tag, _> = s.try_into();
|
||||
assert_eq!(
|
||||
tag.unwrap_err().to_string(),
|
||||
format!("invalid tag \"{}\"", s)
|
||||
);
|
||||
}
|
||||
}
|
||||
554
taskchampion/src/task/task.rs
Normal file
554
taskchampion/src/task/task.rs
Normal file
@@ -0,0 +1,554 @@
|
||||
use super::{Status, SyntheticTag, Tag};
|
||||
use crate::replica::Replica;
|
||||
use crate::storage::TaskMap;
|
||||
use chrono::prelude::*;
|
||||
use log::trace;
|
||||
use std::convert::AsRef;
|
||||
use std::convert::TryInto;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A task, as publicly exposed by this crate.
|
||||
///
|
||||
/// Note that Task objects represent a snapshot of the task at a moment in time, and are not
|
||||
/// protected by the atomicity of the backend storage. Concurrent modifications are safe,
|
||||
/// but a Task that is cached for more than a few seconds may cause the user to see stale
|
||||
/// data. Fetch, use, and drop Tasks quickly.
|
||||
///
|
||||
/// This struct contains only getters for various values on the task. The `into_mut` method returns
|
||||
/// a TaskMut which can be used to modify the task.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Task {
|
||||
uuid: Uuid,
|
||||
taskmap: TaskMap,
|
||||
}
|
||||
|
||||
/// A mutable task, with setter methods. Most methods are simple setters and not further
|
||||
/// described. Calling a setter will update the Replica, as well as the included Task.
|
||||
pub struct TaskMut<'r> {
|
||||
task: Task,
|
||||
replica: &'r mut Replica,
|
||||
updated_modified: bool,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub(crate) fn new(uuid: Uuid, taskmap: TaskMap) -> Task {
|
||||
Task { uuid, taskmap }
|
||||
}
|
||||
|
||||
pub fn get_uuid(&self) -> Uuid {
|
||||
self.uuid
|
||||
}
|
||||
|
||||
pub fn get_taskmap(&self) -> &TaskMap {
|
||||
&self.taskmap
|
||||
}
|
||||
|
||||
/// Prepare to mutate this task, requiring a mutable Replica
|
||||
/// in order to update the data it contains.
|
||||
pub fn into_mut(self, replica: &mut Replica) -> TaskMut {
|
||||
TaskMut {
|
||||
task: self,
|
||||
replica,
|
||||
updated_modified: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_status(&self) -> Status {
|
||||
self.taskmap
|
||||
.get("status")
|
||||
.map(|s| Status::from_taskmap(s))
|
||||
.unwrap_or(Status::Pending)
|
||||
}
|
||||
|
||||
pub fn get_description(&self) -> &str {
|
||||
self.taskmap
|
||||
.get("description")
|
||||
.map(|s| s.as_ref())
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
/// Get the wait time. If this value is set, it will be returned, even
|
||||
/// if it is in the past.
|
||||
pub fn get_wait(&self) -> Option<DateTime<Utc>> {
|
||||
self.get_timestamp("wait")
|
||||
}
|
||||
|
||||
/// Determine whether this task is waiting now.
|
||||
pub fn is_waiting(&self) -> bool {
|
||||
if let Some(ts) = self.get_wait() {
|
||||
return ts > Utc::now();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// 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()
|
||||
.any(|(k, v)| k.starts_with("start.") && v.is_empty())
|
||||
}
|
||||
|
||||
/// Determine whether a given synthetic tag is present on this task. All other
|
||||
/// synthetic tag calculations are based on this one.
|
||||
fn has_synthetic_tag(&self, synth: &SyntheticTag) -> bool {
|
||||
match synth {
|
||||
SyntheticTag::Waiting => self.is_waiting(),
|
||||
SyntheticTag::Active => self.is_active(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this task has the given tag
|
||||
pub fn has_tag(&self, tag: &Tag) -> bool {
|
||||
match tag {
|
||||
Tag::User(s) => self.taskmap.contains_key(&format!("tag.{}", s)),
|
||||
Tag::Synthetic(st) => self.has_synthetic_tag(st),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the task's tags
|
||||
pub fn get_tags(&self) -> impl Iterator<Item = Tag> + '_ {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
self.taskmap
|
||||
.iter()
|
||||
.filter_map(|(k, _)| {
|
||||
if let Some(tag) = k.strip_prefix("tag.") {
|
||||
if let Ok(tag) = tag.try_into() {
|
||||
return Some(tag);
|
||||
}
|
||||
// note that invalid "tag.*" are ignored
|
||||
}
|
||||
None
|
||||
})
|
||||
.chain(
|
||||
SyntheticTag::iter()
|
||||
.filter(move |st| self.has_synthetic_tag(st))
|
||||
.map(|st| Tag::Synthetic(st)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_modified(&self) -> Option<DateTime<Utc>> {
|
||||
self.get_timestamp("modified")
|
||||
}
|
||||
|
||||
// -- utility functions
|
||||
|
||||
fn get_timestamp(&self, property: &str) -> Option<DateTime<Utc>> {
|
||||
if let Some(ts) = self.taskmap.get(property) {
|
||||
if let Ok(ts) = ts.parse() {
|
||||
return Some(Utc.timestamp(ts, 0));
|
||||
}
|
||||
// if the value does not parse as an integer, default to None
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> TaskMut<'r> {
|
||||
/// Get the immutable version of this object. Note that TaskMut [`std::ops::Deref`]s to
|
||||
/// [`crate::task::Task`], so all of that struct's getter methods can be used on TaskMut.
|
||||
pub fn into_immut(self) -> Task {
|
||||
self.task
|
||||
}
|
||||
|
||||
/// Set the task's status. This also adds the task to the working set if the
|
||||
/// new status puts it in that set.
|
||||
pub fn set_status(&mut self, status: Status) -> anyhow::Result<()> {
|
||||
if status == Status::Pending {
|
||||
let uuid = self.uuid;
|
||||
self.replica.add_to_working_set(uuid)?;
|
||||
}
|
||||
self.set_string("status", Some(String::from(status.to_taskmap())))
|
||||
}
|
||||
|
||||
pub fn set_description(&mut self, description: String) -> anyhow::Result<()> {
|
||||
self.set_string("description", Some(description))
|
||||
}
|
||||
|
||||
pub fn set_wait(&mut self, wait: Option<DateTime<Utc>>) -> anyhow::Result<()> {
|
||||
self.set_timestamp("wait", wait)
|
||||
}
|
||||
|
||||
pub fn set_modified(&mut self, modified: DateTime<Utc>) -> anyhow::Result<()> {
|
||||
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) -> anyhow::Result<()> {
|
||||
if self.is_active() {
|
||||
return Ok(());
|
||||
}
|
||||
let k = format!("start.{}", Utc::now().timestamp());
|
||||
self.set_string(k, Some(String::from("")))
|
||||
}
|
||||
|
||||
/// Stop the task by adding the current timestamp to all un-resolved "start.<timestamp>" keys.
|
||||
pub fn stop(&mut self) -> anyhow::Result<()> {
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Add a tag to this task. Does nothing if the tag is already present.
|
||||
pub fn add_tag(&mut self, tag: &Tag) -> anyhow::Result<()> {
|
||||
if let Tag::Synthetic(_) = tag {
|
||||
anyhow::bail!("Synthetic tags cannot be modified");
|
||||
}
|
||||
self.set_string(format!("tag.{}", tag), Some("".to_owned()))
|
||||
}
|
||||
|
||||
/// Remove a tag from this task. Does nothing if the tag is not present.
|
||||
pub fn remove_tag(&mut self, tag: &Tag) -> anyhow::Result<()> {
|
||||
if let Tag::Synthetic(_) = tag {
|
||||
anyhow::bail!("Synthetic tags cannot be modified");
|
||||
}
|
||||
self.set_string(format!("tag.{}", tag), None)
|
||||
}
|
||||
|
||||
// -- utility functions
|
||||
|
||||
fn lastmod(&mut self) -> anyhow::Result<()> {
|
||||
if !self.updated_modified {
|
||||
let now = format!("{}", Utc::now().timestamp());
|
||||
self.replica
|
||||
.update_task(self.task.uuid, "modified", Some(now.clone()))?;
|
||||
trace!("task {}: set property modified={:?}", self.task.uuid, now);
|
||||
self.task.taskmap.insert(String::from("modified"), now);
|
||||
self.updated_modified = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_string<S: Into<String>>(
|
||||
&mut self,
|
||||
property: S,
|
||||
value: Option<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let property = property.into();
|
||||
self.lastmod()?;
|
||||
self.replica
|
||||
.update_task(self.task.uuid, &property, value.as_ref())?;
|
||||
|
||||
if let Some(v) = value {
|
||||
trace!("task {}: set property {}={:?}", self.task.uuid, property, v);
|
||||
self.task.taskmap.insert(property, v);
|
||||
} else {
|
||||
trace!("task {}: remove property {}", self.task.uuid, property);
|
||||
self.task.taskmap.remove(&property);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_timestamp(
|
||||
&mut self,
|
||||
property: &str,
|
||||
value: Option<DateTime<Utc>>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.lastmod()?;
|
||||
if let Some(value) = value {
|
||||
let ts = format!("{}", value.timestamp());
|
||||
self.replica
|
||||
.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) -> anyhow::Result<()> {
|
||||
let uuid = self.uuid;
|
||||
let task = self.replica.get_task(uuid)?.unwrap();
|
||||
self.task.taskmap = task.taskmap;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> std::ops::Deref for TaskMut<'r> {
|
||||
type Target = Task;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.task
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Create a user tag, without checking its validity
|
||||
fn utag(name: &'static str) -> Tag {
|
||||
Tag::User(name.into())
|
||||
}
|
||||
|
||||
/// Create a synthetic tag
|
||||
fn stag(synth: SyntheticTag) -> Tag {
|
||||
Tag::Synthetic(synth)
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wait_not_set() {
|
||||
let task = Task::new(Uuid::new_v4(), TaskMap::new());
|
||||
|
||||
assert!(!task.is_waiting());
|
||||
assert_eq!(task.get_wait(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wait_in_past() {
|
||||
let ts = Utc.ymd(1970, 1, 1).and_hms(0, 0, 0);
|
||||
let task = Task::new(
|
||||
Uuid::new_v4(),
|
||||
vec![(String::from("wait"), format!("{}", ts.timestamp()))]
|
||||
.drain(..)
|
||||
.collect(),
|
||||
);
|
||||
dbg!(&task);
|
||||
|
||||
assert!(!task.is_waiting());
|
||||
assert_eq!(task.get_wait(), Some(ts));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wait_in_future() {
|
||||
let ts = Utc.ymd(3000, 1, 1).and_hms(0, 0, 0);
|
||||
let task = Task::new(
|
||||
Uuid::new_v4(),
|
||||
vec![(String::from("wait"), format!("{}", ts.timestamp()))]
|
||||
.drain(..)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
assert!(task.is_waiting());
|
||||
assert_eq!(task.get_wait(), Some(ts));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_tag() {
|
||||
let task = Task::new(
|
||||
Uuid::new_v4(),
|
||||
vec![
|
||||
(String::from("tag.abc"), String::from("")),
|
||||
(String::from("start.1234"), String::from("")),
|
||||
]
|
||||
.drain(..)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
assert!(task.has_tag(&utag("abc")));
|
||||
assert!(!task.has_tag(&utag("def")));
|
||||
assert!(task.has_tag(&stag(SyntheticTag::Active)));
|
||||
assert!(!task.has_tag(&stag(SyntheticTag::Waiting)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tags() {
|
||||
let task = Task::new(
|
||||
Uuid::new_v4(),
|
||||
vec![
|
||||
(String::from("tag.abc"), String::from("")),
|
||||
(String::from("tag.def"), String::from("")),
|
||||
// set `wait` so the synthetic tag WAITING is present
|
||||
(String::from("wait"), String::from("33158909732")),
|
||||
]
|
||||
.drain(..)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let mut tags: Vec<_> = task.get_tags().collect();
|
||||
tags.sort();
|
||||
assert_eq!(
|
||||
tags,
|
||||
vec![utag("abc"), utag("def"), stag(SyntheticTag::Waiting),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tags_invalid_tags() {
|
||||
let task = Task::new(
|
||||
Uuid::new_v4(),
|
||||
vec![
|
||||
(String::from("tag.ok"), String::from("")),
|
||||
(String::from("tag."), String::from("")),
|
||||
(String::from("tag.123"), String::from("")),
|
||||
(String::from("tag.a!!"), String::from("")),
|
||||
]
|
||||
.drain(..)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// only "ok" is OK
|
||||
let tags: Vec<_> = task.get_tags().collect();
|
||||
assert_eq!(tags, vec![utag("ok")]);
|
||||
}
|
||||
|
||||
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]
|
||||
fn test_add_tags() {
|
||||
with_mut_task(|mut task| {
|
||||
task.add_tag(&utag("abc")).unwrap();
|
||||
assert!(task.taskmap.contains_key("tag.abc"));
|
||||
task.reload().unwrap();
|
||||
assert!(task.taskmap.contains_key("tag.abc"));
|
||||
// redundant add has no effect..
|
||||
task.add_tag(&utag("abc")).unwrap();
|
||||
assert!(task.taskmap.contains_key("tag.abc"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_tags() {
|
||||
with_mut_task(|mut task| {
|
||||
task.add_tag(&utag("abc")).unwrap();
|
||||
task.reload().unwrap();
|
||||
assert!(task.taskmap.contains_key("tag.abc"));
|
||||
|
||||
task.remove_tag(&utag("abc")).unwrap();
|
||||
assert!(!task.taskmap.contains_key("tag.abc"));
|
||||
// redundant remove has no effect..
|
||||
task.remove_tag(&utag("abc")).unwrap();
|
||||
assert!(!task.taskmap.contains_key("tag.abc"));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user