From e5625e1597bc57e4713d542b825da6429ccf2d1b Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 1 Feb 2022 02:45:28 +0000 Subject: [PATCH] entry and wait time support --- Cargo.lock | 1 + integration-tests/src/bindings_tests/task.c | 63 ++++++++++++++++++ lib/Cargo.toml | 3 +- lib/build.rs | 2 +- lib/src/task.rs | 74 +++++++++++++++++++-- lib/taskchampion.h | 33 ++++++++- taskchampion/src/replica.rs | 2 +- taskchampion/src/task/task.rs | 26 +++++++- 8 files changed, 190 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 094b83445..ad2920754 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3036,6 +3036,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cbindgen", + "chrono", "libc", "taskchampion", "uuid", diff --git a/integration-tests/src/bindings_tests/task.c b/integration-tests/src/bindings_tests/task.c index 7fcb9a683..8b264e41f 100644 --- a/integration-tests/src/bindings_tests/task.c +++ b/integration-tests/src/bindings_tests/task.c @@ -111,6 +111,67 @@ static void test_task_get_set_description(void) { tc_replica_free(rep); } +// updating entry on a task works +static void test_task_get_set_entry(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + // creation of a task sets entry to current time + TEST_ASSERT_NOT_EQUAL(0, tc_task_get_entry(task)); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_entry(task, 1643679997)); + TEST_ASSERT_EQUAL(1643679997, tc_task_get_entry(task)); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_entry(task, 0)); + TEST_ASSERT_EQUAL(0, tc_task_get_entry(task)); + + tc_task_free(task); + + tc_replica_free(rep); +} + +// updating wait on a task works +static void test_task_get_set_wait_and_is_waiting(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + TCTask *task = tc_replica_new_task( + rep, + TC_STATUS_PENDING, + tc_string_borrow("my task")); + TEST_ASSERT_NOT_NULL(task); + + // wait is not set on creation + TEST_ASSERT_EQUAL(0, tc_task_get_wait(task)); + TEST_ASSERT_FALSE(tc_task_is_waiting(task)); + + tc_task_to_mut(task, rep); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 3643679997)); // 2085 + TEST_ASSERT_EQUAL(3643679997, tc_task_get_wait(task)); + TEST_ASSERT_TRUE(tc_task_is_waiting(task)); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 643679997)); // THE PAST! + TEST_ASSERT_EQUAL(643679997, tc_task_get_wait(task)); + TEST_ASSERT_FALSE(tc_task_is_waiting(task)); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_set_wait(task, 0)); + TEST_ASSERT_EQUAL(0, tc_task_get_wait(task)); + TEST_ASSERT_FALSE(tc_task_is_waiting(task)); + + tc_task_free(task); + + tc_replica_free(rep); +} + // starting and stopping a task works, as seen by tc_task_is_active static void test_task_start_stop_is_active(void) { TCReplica *rep = tc_replica_new_in_memory(); @@ -187,6 +248,8 @@ int task_tests(void) { RUN_TEST(test_task_free_mutable_task); RUN_TEST(test_task_get_set_status); RUN_TEST(test_task_get_set_description); + RUN_TEST(test_task_get_set_entry); + RUN_TEST(test_task_get_set_wait_and_is_waiting); RUN_TEST(test_task_start_stop_is_active); RUN_TEST(task_task_add_tag); return UNITY_END(); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b8545df90..66b8c6051 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -10,8 +10,9 @@ crate-type = ["cdylib"] [dependencies] libc = "0.2.113" +chrono = "^0.4.10" taskchampion = { path = "../taskchampion" } -uuid = { version = "^0.8.2", features = ["serde", "v4"] } +uuid = { version = "^0.8.2", features = ["v4"] } anyhow = "1.0" [build-dependencies] diff --git a/lib/build.rs b/lib/build.rs index 462b707a4..f8f75d3b5 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -10,7 +10,7 @@ fn main() { .with_config(Config { language: Language::C, cpp_compat: true, - sys_includes: vec!["stdbool.h".into(), "stdint.h".into()], + sys_includes: vec!["stdbool.h".into(), "stdint.h".into(), "time.h".into()], usize_is_size_t: true, no_includes: true, enumeration: EnumConfig { diff --git a/lib/src/task.rs b/lib/src/task.rs index 3558ccf20..03318d1f9 100644 --- a/lib/src/task.rs +++ b/lib/src/task.rs @@ -2,6 +2,7 @@ use crate::util::err_to_tcstring; use crate::{ replica::TCReplica, result::TCResult, status::TCStatus, string::TCString, uuid::TCUuid, }; +use chrono::{DateTime, TimeZone, Utc}; use std::convert::TryFrom; use std::ops::Deref; use std::str::FromStr; @@ -13,8 +14,9 @@ use taskchampion::{Tag, Task, TaskMut}; /// to make any changes, and doing so requires exclusive access to the replica /// until the task is freed or converted back to immutable mode. /// -/// A task carries no reference to the replica that created it, and can -/// be used until it is freed or converted to a TaskMut. +/// An immutable task carries no reference to the replica that created it, and can be used until it +/// is freed or converted to a TaskMut. A mutable task carries a reference to the replica and +/// must be freed or made immutable before the replica is freed. /// /// All `tc_task_..` functions taking a task as an argument require that it not be NULL. /// @@ -178,6 +180,22 @@ impl TryFrom> for Tag { } } +/// Convert a DateTime to a libc::time_t, or zero if not set. +fn to_time_t(timestamp: Option>) -> libc::time_t { + timestamp + .map(|ts| ts.timestamp() as libc::time_t) + .unwrap_or(0 as libc::time_t) +} + +/// Convert a libc::time_t to Option>, treating time zero as None +fn to_datetime(time: libc::time_t) -> Option> { + if time == 0 { + None + } else { + Some(Utc.timestamp(time as i64, 0)) + } +} + /// Convert an immutable task into a mutable task. /// /// The task must not be NULL. It is modified in-place, and becomes mutable. @@ -245,14 +263,29 @@ pub extern "C" fn tc_task_get_description<'a>(task: *mut TCTask) -> *mut TCStrin }) } -// TODO: tc_task_get_entry -// TODO: tc_task_get_wait +/// Get the entry timestamp for a task (when it was created), or 0 if not set. +#[no_mangle] +pub extern "C" fn tc_task_get_entry<'a>(task: *mut TCTask) -> libc::time_t { + wrap(task, |task| to_time_t(task.get_entry())) +} + +/// Get the wait timestamp for a task, or 0 if not set. +#[no_mangle] +pub extern "C" fn tc_task_get_wait<'a>(task: *mut TCTask) -> libc::time_t { + wrap(task, |task| to_time_t(task.get_wait())) +} + // TODO: tc_task_get_modified -// TODO: tc_task_is_waiting + +/// Check if a task is waiting. +#[no_mangle] +pub extern "C" fn tc_task_is_waiting(task: *mut TCTask) -> bool { + wrap(task, |task| task.is_waiting()) +} /// Check if a task is active (started and not stopped). #[no_mangle] -pub extern "C" fn tc_task_is_active<'a>(task: *mut TCTask) -> bool { +pub extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { wrap(task, |task| task.is_active()) } @@ -314,7 +347,34 @@ pub extern "C" fn tc_task_set_description<'a>( ) } -// TODO: tc_task_set_entry +/// Set a mutable task's entry (creation time). Pass entry=0 to unset +/// the entry field. +#[no_mangle] +pub extern "C" fn tc_task_set_entry<'a>(task: *mut TCTask, entry: libc::time_t) -> TCResult { + wrap_mut( + task, + |task| { + task.set_entry(to_datetime(entry))?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + +/// Set a mutable task's wait (creation time). Pass wait=0 to unset the +/// wait field. +#[no_mangle] +pub extern "C" fn tc_task_set_wait<'a>(task: *mut TCTask, wait: libc::time_t) -> TCResult { + wrap_mut( + task, + |task| { + task.set_wait(to_datetime(wait))?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} + // TODO: tc_task_set_wait // TODO: tc_task_set_modified diff --git a/lib/taskchampion.h b/lib/taskchampion.h index a0b9e46d7..f102d03aa 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -1,5 +1,6 @@ #include #include +#include /** * Length, in bytes, of the string representation of a UUID (without NUL terminator) @@ -74,8 +75,9 @@ typedef struct TCString TCString; * to make any changes, and doing so requires exclusive access to the replica * until the task is freed or converted back to immutable mode. * - * A task carries no reference to the replica that created it, and can - * be used until it is freed or converted to a TaskMut. + * An immutable task carries no reference to the replica that created it, and can be used until it + * is freed or converted to a TaskMut. A mutable task carries a reference to the replica and + * must be freed or made immutable before the replica is freed. * * All `tc_task_..` functions taking a task as an argument require that it not be NULL. * @@ -264,6 +266,21 @@ enum TCStatus tc_task_get_status(struct TCTask *task); */ struct TCString *tc_task_get_description(struct TCTask *task); +/** + * Get the entry timestamp for a task (when it was created), or 0 if not set. + */ +time_t tc_task_get_entry(struct TCTask *task); + +/** + * Get the wait timestamp for a task, or 0 if not set. + */ +time_t tc_task_get_wait(struct TCTask *task); + +/** + * Check if a task is waiting. + */ +bool tc_task_is_waiting(struct TCTask *task); + /** * Check if a task is active (started and not stopped). */ @@ -285,6 +302,18 @@ TCResult tc_task_set_status(struct TCTask *task, enum TCStatus status); */ TCResult tc_task_set_description(struct TCTask *task, struct TCString *description); +/** + * Set a mutable task's entry (creation time). Pass entry=0 to unset + * the entry field. + */ +TCResult tc_task_set_entry(struct TCTask *task, time_t entry); + +/** + * Set a mutable task's wait (creation time). Pass wait=0 to unset the + * wait field. + */ +TCResult tc_task_set_wait(struct TCTask *task, time_t wait); + /** * Start a task. */ diff --git a/taskchampion/src/replica.rs b/taskchampion/src/replica.rs index b85cb84c7..2dd1e4887 100644 --- a/taskchampion/src/replica.rs +++ b/taskchampion/src/replica.rs @@ -110,7 +110,7 @@ impl Replica { let mut task = Task::new(uuid, taskmap).into_mut(self); task.set_description(description)?; task.set_status(status)?; - task.set_entry(Utc::now())?; + task.set_entry(Some(Utc::now()))?; trace!("task {} created", uuid); Ok(task.into_immut()) } diff --git a/taskchampion/src/task/task.rs b/taskchampion/src/task/task.rs index 82bafb7d7..d5517f5b4 100644 --- a/taskchampion/src/task/task.rs +++ b/taskchampion/src/task/task.rs @@ -120,6 +120,10 @@ impl Task { .unwrap_or("") } + pub fn get_entry(&self) -> Option> { + self.get_timestamp(Prop::Entry.as_ref()) + } + pub fn get_priority(&self) -> Priority { self.taskmap .get(Prop::Status.as_ref()) @@ -299,8 +303,8 @@ impl<'r> TaskMut<'r> { self.set_string(Prop::Description.as_ref(), Some(description)) } - pub(crate) fn set_entry(&mut self, entry: DateTime) -> anyhow::Result<()> { - self.set_timestamp(Prop::Entry.as_ref(), Some(entry)) + pub fn set_entry(&mut self, entry: Option>) -> anyhow::Result<()> { + self.set_timestamp(Prop::Entry.as_ref(), entry) } pub fn set_wait(&mut self, wait: Option>) -> anyhow::Result<()> { @@ -526,6 +530,24 @@ mod test { assert!(!task.is_active()); } + #[test] + fn test_entry_not_set() { + let task = Task::new(Uuid::new_v4(), TaskMap::new()); + assert_eq!(task.get_entry(), None); + } + + #[test] + fn test_entry_set() { + let ts = Utc.ymd(1980, 1, 1).and_hms(0, 0, 0); + let task = Task::new( + Uuid::new_v4(), + vec![(String::from("entry"), format!("{}", ts.timestamp()))] + .drain(..) + .collect(), + ); + assert_eq!(task.get_entry(), Some(ts)); + } + #[test] fn test_wait_not_set() { let task = Task::new(Uuid::new_v4(), TaskMap::new());