diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 7e598695b..423d8dc25 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -40,12 +40,74 @@ static void test_replica_add_undo_point(void) { tc_replica_free(rep); } -// rebuilding working set succeeds -static void test_replica_rebuild_working_set(void) { +// working set operations succeed +static void test_replica_working_set(void) { + TCWorkingSet *ws; + TCTask *task1, *task2, *task3; + TCUuid uuid, uuid1, uuid2, uuid3; + TCReplica *rep = tc_replica_new_in_memory(); TEST_ASSERT_NULL(tc_replica_error(rep)); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, true)); TEST_ASSERT_NULL(tc_replica_error(rep)); + + ws = tc_replica_working_set(rep); + TEST_ASSERT_EQUAL(0, tc_working_set_len(ws)); + tc_working_set_free(ws); + + task1 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task1")); + TEST_ASSERT_NOT_NULL(task1); + uuid1 = tc_task_get_uuid(task1); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, true)); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + task2 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task2")); + TEST_ASSERT_NOT_NULL(task2); + uuid2 = tc_task_get_uuid(task2); + + task3 = tc_replica_new_task(rep, TC_STATUS_PENDING, tc_string_borrow("task3")); + TEST_ASSERT_NOT_NULL(task3); + uuid3 = tc_task_get_uuid(task3); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, false)); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + // finish task2 to leave a "hole" + tc_task_to_mut(task2, rep); + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_task_done(task2)); + tc_task_to_immut(task2); + + TEST_ASSERT_EQUAL(TC_RESULT_OK, tc_replica_rebuild_working_set(rep, false)); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + tc_task_free(task1); + tc_task_free(task2); + tc_task_free(task3); + + // working set should now be + // 0 -> None + // 1 -> uuid1 + // 2 -> None + // 3 -> uuid3 + ws = tc_replica_working_set(rep); + TEST_ASSERT_EQUAL(2, tc_working_set_len(ws)); + TEST_ASSERT_EQUAL(3, tc_working_set_largest_index(ws)); + + TEST_ASSERT_FALSE(tc_working_set_by_index(ws, 0, &uuid)); + TEST_ASSERT_TRUE(tc_working_set_by_index(ws, 1, &uuid)); + TEST_ASSERT_EQUAL_MEMORY(uuid1.bytes, uuid.bytes, sizeof(uuid)); + TEST_ASSERT_FALSE(tc_working_set_by_index(ws, 2, &uuid)); + TEST_ASSERT_TRUE(tc_working_set_by_index(ws, 3, &uuid)); + TEST_ASSERT_EQUAL_MEMORY(uuid3.bytes, uuid.bytes, sizeof(uuid)); + + TEST_ASSERT_EQUAL(1, tc_working_set_by_uuid(ws, uuid1)); + TEST_ASSERT_EQUAL(0, tc_working_set_by_uuid(ws, uuid2)); + TEST_ASSERT_EQUAL(3, tc_working_set_by_uuid(ws, uuid3)); + + tc_working_set_free(ws); + tc_replica_free(rep); } @@ -209,7 +271,7 @@ int replica_tests(void) { RUN_TEST(test_replica_creation_disk); RUN_TEST(test_replica_undo_empty); RUN_TEST(test_replica_add_undo_point); - RUN_TEST(test_replica_rebuild_working_set); + RUN_TEST(test_replica_working_set); RUN_TEST(test_replica_undo_empty_null_undone_out); RUN_TEST(test_replica_task_creation); RUN_TEST(test_replica_all_tasks); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 5bc7629a3..b49acf77c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -14,6 +14,7 @@ pub mod string; pub mod task; pub mod uda; pub mod uuid; +pub mod workingset; pub(crate) mod types { pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList}; @@ -24,4 +25,5 @@ pub(crate) mod types { pub(crate) use crate::task::{TCTask, TCTaskList}; pub(crate) use crate::uda::{TCUDAList, TCUDA, UDA}; pub(crate) use crate::uuid::{TCUuid, TCUuidList}; + pub(crate) use crate::workingset::TCWorkingSet; } diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 99f07639d..b89df1091 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -177,7 +177,21 @@ pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUui ) } -// TODO: tc_replica_working_set +/// Get the current working set for this replica. +/// +/// Returns NULL on error. +#[no_mangle] +pub unsafe extern "C" fn tc_replica_working_set(rep: *mut TCReplica) -> *mut TCWorkingSet { + wrap( + rep, + |rep| { + let ws = rep.working_set()?; + // SAFETY: caller promises to free this task + Ok(unsafe { TCWorkingSet::return_val(ws.into()) }) + }, + std::ptr::null_mut(), + ) +} /// Get an existing task by its UUID. /// diff --git a/lib/src/workingset.rs b/lib/src/workingset.rs new file mode 100644 index 000000000..91f73bbd0 --- /dev/null +++ b/lib/src/workingset.rs @@ -0,0 +1,87 @@ +use crate::traits::*; +use crate::types::*; +use taskchampion::{Uuid, WorkingSet}; + +/// A TCWorkingSet represents a snapshot of the working set for a replica. It is not automatically +/// updated based on changes in the replica. Its lifetime is independent of the replica and it can +/// be freed at any time. +/// +/// To iterate over a working set, search indexes 1 through largest_index. +pub struct TCWorkingSet(WorkingSet); + +impl PassByPointer for TCWorkingSet {} + +impl From for TCWorkingSet { + fn from(ws: WorkingSet) -> TCWorkingSet { + TCWorkingSet(ws) + } +} + +/// Utility function to get a shared reference to the underlying WorkingSet. +fn wrap<'a, T, F>(ws: *mut TCWorkingSet, f: F) -> T +where + F: FnOnce(&WorkingSet) -> T, +{ + // SAFETY: + // - ws is not null (promised by caller) + // - ws outlives 'a (promised by caller) + let tcws: &'a TCWorkingSet = unsafe { TCWorkingSet::from_arg_ref(ws) }; + f(&tcws.0) +} + +/// Get the working set's length, or the number of UUIDs it contains. +#[no_mangle] +pub unsafe extern "C" fn tc_working_set_len(ws: *mut TCWorkingSet) -> usize { + wrap(ws, |ws| ws.len()) +} + +/// Get the working set's largest index. +#[no_mangle] +pub unsafe extern "C" fn tc_working_set_largest_index(ws: *mut TCWorkingSet) -> usize { + wrap(ws, |ws| ws.largest_index()) +} + +/// Get the UUID for the task at the given index. Returns true if the UUID exists in the working +/// set. If not, returns false and does not change uuid_out. +#[no_mangle] +pub unsafe extern "C" fn tc_working_set_by_index( + ws: *mut TCWorkingSet, + index: usize, + uuid_out: *mut TCUuid, +) -> bool { + debug_assert!(!uuid_out.is_null()); + wrap(ws, |ws| { + if let Some(uuid) = ws.by_index(index) { + // SAFETY: + // - uuid_out is not NULL (promised by caller) + // - alignment is not required + unsafe { TCUuid::to_arg_out(uuid, uuid_out) }; + true + } else { + false + } + }) +} + +/// Get the working set index for the task with the given UUID. Returns 0 if the task is not in +/// the working set. +#[no_mangle] +pub unsafe extern "C" fn tc_working_set_by_uuid(ws: *mut TCWorkingSet, uuid: TCUuid) -> usize { + wrap(ws, |ws| { + // SAFETY: + // - tcuuid is a valid TCUuid (all byte patterns are valid) + let uuid: Uuid = unsafe { TCUuid::from_arg(uuid) }; + ws.by_uuid(uuid).unwrap_or(0) + }) +} + +/// Free a TCWorkingSet. The given value must not be NULL. The value must not be used after this +/// function returns, and must not be freed more than once. +#[no_mangle] +pub unsafe extern "C" fn tc_working_set_free(ws: *mut TCWorkingSet) { + // SAFETY: + // - rep is not NULL (promised by caller) + // - caller will not use the TCWorkingSet after this (promised by caller) + let ws = unsafe { TCWorkingSet::take_from_arg(ws) }; + drop(ws); +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index bc61b1e40..79b25ba35 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -117,6 +117,15 @@ typedef struct TCString TCString; */ typedef struct TCTask TCTask; +/** + * A TCWorkingSet represents a snapshot of the working set for a replica. It is not automatically + * updated based on changes in the replica. Its lifetime is independent of the replica and it can + * be freed at any time. + * + * To iterate over a working set, search indexes 1 through largest_index. + */ +typedef struct TCWorkingSet TCWorkingSet; + /** * TCAnnotation contains the details of an annotation. */ @@ -309,6 +318,13 @@ struct TCTaskList tc_replica_all_tasks(struct TCReplica *rep); */ struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep); +/** + * Get the current working set for this replica. + * + * Returns NULL on error. + */ +struct TCWorkingSet *tc_replica_working_set(struct TCReplica *rep); + /** * Get an existing task by its UUID. * @@ -721,6 +737,34 @@ TCResult tc_uuid_from_str(struct TCString *s, struct TCUuid *uuid_out); */ void tc_uuid_list_free(struct TCUuidList *tcuuids); +/** + * Get the working set's length, or the number of UUIDs it contains. + */ +size_t tc_working_set_len(struct TCWorkingSet *ws); + +/** + * Get the working set's largest index. + */ +size_t tc_working_set_largest_index(struct TCWorkingSet *ws); + +/** + * Get the UUID for the task at the given index. Returns true if the UUID exists in the working + * set. If not, returns false and does not change uuid_out. + */ +bool tc_working_set_by_index(struct TCWorkingSet *ws, size_t index, struct TCUuid *uuid_out); + +/** + * Get the working set index for the task with the given UUID. Returns 0 if the task is not in + * the working set. + */ +size_t tc_working_set_by_uuid(struct TCWorkingSet *ws, struct TCUuid uuid); + +/** + * Free a TCWorkingSet. The given value must not be NULL. The value must not be used after this + * function returns, and must not be freed more than once. + */ +void tc_working_set_free(struct TCWorkingSet *ws); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/taskchampion/src/workingset.rs b/taskchampion/src/workingset.rs index ea746a72b..8bd0cce53 100644 --- a/taskchampion/src/workingset.rs +++ b/taskchampion/src/workingset.rs @@ -38,6 +38,11 @@ impl WorkingSet { self.by_index.iter().filter(|e| e.is_some()).count() } + /// Get the largest index in the working set, or zero if the set is empty. + pub fn largest_index(&self) -> usize { + self.by_index.len().saturating_sub(1) + } + /// True if the length is zero pub fn is_empty(&self) -> bool { self.by_index.iter().all(|e| e.is_none()) @@ -103,6 +108,21 @@ mod test { assert_eq!(ws.is_empty(), true); } + #[test] + fn test_largest_index() { + let (uuid1, uuid2, ws) = make(); + assert_eq!(ws.largest_index(), 0); + + let ws = WorkingSet::new(vec![None, Some(uuid1)]); + assert_eq!(ws.largest_index(), 1); + + let ws = WorkingSet::new(vec![None, Some(uuid1), None, Some(uuid2)]); + assert_eq!(ws.largest_index(), 3); + + let ws = WorkingSet::new(vec![None, Some(uuid1), None, Some(uuid2), None]); + assert_eq!(ws.largest_index(), 4); + } + #[test] fn test_by_index() { let (uuid1, uuid2, ws) = make();