diff --git a/src/replica.rs b/src/replica.rs index 0e8c4dd..810bdf9 100644 --- a/src/replica.rs +++ b/src/replica.rs @@ -47,7 +47,7 @@ impl Replica { let task = self .0 .create_task(Uuid::parse_str(&uuid)?, ops.as_mut()) - .map(Task)?; + .map(Task::from)?; Ok(task) } @@ -57,7 +57,7 @@ impl Replica { .0 .all_tasks()? .into_iter() - .map(|(key, value)| (key.to_string(), Task(value))) + .map(|(key, value)| (key.to_string(), value.into())) .collect()) } @@ -66,7 +66,7 @@ impl Replica { .0 .all_task_data()? .into_iter() - .map(|(key, value)| (key.to_string(), TaskData(value))) + .map(|(key, value)| (key.to_string(), TaskData::from(value))) .collect()) } /// Get a list of all uuids for tasks in the replica. @@ -101,14 +101,14 @@ impl Replica { Ok(self .0 .get_task(Uuid::parse_str(&uuid).unwrap()) - .map(|opt| opt.map(Task))?) + .map(|opt| opt.map(Task::from))?) } pub fn get_task_data(&mut self, uuid: String) -> anyhow::Result> { Ok(self .0 .get_task_data(Uuid::parse_str(&uuid)?) - .map(|opt| opt.map(TaskData))?) + .map(|opt| opt.map(TaskData::from))?) } /// Sync with a server crated from `ServerConfig::Local`. diff --git a/src/task/annotation.rs b/src/task/annotation.rs index 13ff320..4899b88 100644 --- a/src/task/annotation.rs +++ b/src/task/annotation.rs @@ -1,36 +1,48 @@ -use chrono::DateTime; +use chrono::{DateTime, Utc}; use pyo3::prelude::*; use taskchampion::Annotation as TCAnnotation; -#[pyclass] + +#[pyclass(frozen, eq)] +#[derive(PartialEq, Eq)] /// An annotation for the task -pub struct Annotation(pub(crate) TCAnnotation); +pub struct Annotation(TCAnnotation); #[pymethods] impl Annotation { #[new] - pub fn new() -> Self { - Annotation(TCAnnotation { - entry: DateTime::default(), - description: String::new(), - }) + pub fn new(entry: DateTime, description: String) -> Self { + Annotation(TCAnnotation { entry, description }) } - #[getter] - pub fn entry(&self) -> String { - self.0.entry.to_rfc3339() + + pub fn __repr__(&self) -> String { + format!("{:?}", self.as_ref()) } - #[setter] - pub fn set_entry(&mut self, time: String) { - self.0.entry = DateTime::parse_from_rfc3339(&time).unwrap().into() + #[getter] + pub fn entry(&self) -> DateTime { + self.0.entry } #[getter] pub fn description(&self) -> String { self.0.description.clone() } +} + +impl AsRef for Annotation { + fn as_ref(&self) -> &TCAnnotation { + &self.0 + } +} + +impl From for Annotation { + fn from(value: TCAnnotation) -> Self { + Annotation(value) + } +} - #[setter] - pub fn set_description(&mut self, description: String) { - self.0.description = description +impl From for TCAnnotation { + fn from(value: Annotation) -> Self { + value.0 } } diff --git a/src/task/data.rs b/src/task/data.rs index de0d552..15514e5 100644 --- a/src/task/data.rs +++ b/src/task/data.rs @@ -3,7 +3,7 @@ use pyo3::{exceptions::PyValueError, prelude::*}; use taskchampion::{TaskData as TCTaskData, Uuid}; #[pyclass] -pub struct TaskData(pub(crate) TCTaskData); +pub struct TaskData(TCTaskData); #[pymethods] impl TaskData { @@ -13,7 +13,10 @@ impl TaskData { Ok(TaskData(TCTaskData::create(u, ops.as_mut()))) } - #[getter(uuid)] + pub fn __repr__(&self) -> String { + format!("{:?}", self.0) + } + pub fn get_uuid(&self) -> String { self.0.get_uuid().into() } @@ -35,3 +38,21 @@ impl TaskData { self.0.delete(ops.as_mut()); } } + +impl From for TaskData { + fn from(value: TCTaskData) -> Self { + TaskData(value) + } +} + +impl From for TCTaskData { + fn from(value: TaskData) -> Self { + value.0 + } +} + +impl AsRef for TaskData { + fn as_ref(&self) -> &TCTaskData { + &self.0 + } +} diff --git a/src/task/tag.rs b/src/task/tag.rs index 7216728..466ca16 100644 --- a/src/task/tag.rs +++ b/src/task/tag.rs @@ -1,7 +1,8 @@ use pyo3::{exceptions::PyValueError, prelude::*}; use taskchampion::Tag as TCTag; -#[pyclass] +#[pyclass(frozen, eq, hash)] +#[derive(PartialEq, Eq, Hash)] pub struct Tag(TCTag); #[pymethods] diff --git a/src/task/task.rs b/src/task/task.rs index dcba09b..7fd7cf4 100644 --- a/src/task/task.rs +++ b/src/task/task.rs @@ -4,33 +4,33 @@ use chrono::{DateTime, Utc}; use pyo3::prelude::*; use taskchampion::{Task as TCTask, Uuid}; -// TODO: actually create a front-facing user class, instead of this data blob -#[pyclass] -pub struct Task(pub(crate) TCTask); - -unsafe impl Send for Task {} +// TODO: This type can be send once https://github.com/GothenburgBitFactory/taskchampion/pull/514 +// is available. +#[pyclass(unsendable)] +/// A TaskChampion Task. +/// +/// This type is not Send, so it cannot be used from any thread but the one where it was created. +pub struct Task(TCTask); +#[pymethods] impl Task { - fn to_datetime(s: Option) -> anyhow::Result>> { - s.map(|time| Ok(DateTime::parse_from_rfc3339(&time)?.with_timezone(&chrono::Utc))) - .transpose() + fn __repr__(&self) -> String { + format!("{:?}", self.as_ref()) } -} -#[pymethods] -impl Task { #[allow(clippy::wrong_self_convention)] pub fn into_task_data(&self) -> TaskData { - TaskData(self.0.clone().into_task_data()) + self.0.clone().into_task_data().into() } + /// Get a tasks UUID /// /// Returns: /// str: UUID of a task - // TODO: possibly determine if it's possible to turn this from/into python's UUID instead pub fn get_uuid(&self) -> String { self.0.get_uuid().to_string() } + /// Get a task's status /// Returns: /// Status: Status subtype @@ -38,6 +38,7 @@ impl Task { self.0.get_status().into() } + /// Get a task's description pub fn get_description(&self) -> String { self.0.get_description().to_string() } @@ -47,7 +48,6 @@ impl Task { /// Returns: /// str: RFC3339 timestamp /// None: No timestamp - // Attempt to convert this into a python datetime later on pub fn get_entry(&self) -> Option> { self.0.get_entry() } @@ -68,6 +68,7 @@ impl Task { pub fn get_wait(&self) -> Option> { self.0.get_wait() } + /// Check if the task is waiting /// /// Returns: @@ -83,6 +84,7 @@ impl Task { pub fn is_active(&self) -> bool { self.0.is_active() } + /// Check if the task is blocked /// /// Returns: @@ -90,6 +92,7 @@ impl Task { pub fn is_blocked(&self) -> bool { self.0.is_blocked() } + /// Check if the task is blocking /// /// Returns: @@ -97,6 +100,7 @@ impl Task { pub fn is_blocking(&self) -> bool { self.0.is_blocking() } + /// Check if the task has a tag /// /// Returns: @@ -112,12 +116,13 @@ impl Task { pub fn get_tags(&self) -> Vec { self.0.get_tags().map(Tag::from).collect() } + /// Get task annotations /// /// Returns: /// list[Annotation]: list of task annotations pub fn get_annotations(&self) -> Vec { - self.0.get_annotations().map(Annotation).collect() + self.0.get_annotations().map(Annotation::from).collect() } /// Get a task UDA @@ -133,12 +138,10 @@ impl Task { self.0.get_uda(namespace, key) } - // TODO: this signature is ugly and confising, possibly replace this with a struct in the - // actual code /// get all the task's UDAs /// /// Returns: - /// Uh oh, ew? + /// List of tuples ((namespace, key), value) pub fn get_udas(&self) -> Vec<((&str, &str), &str)> { self.0.get_udas().collect() } @@ -198,21 +201,30 @@ impl Task { } #[pyo3(signature=(entry, ops))] - pub fn set_entry(&mut self, entry: Option, ops: &mut Operations) -> anyhow::Result<()> { - let timestamp = Self::to_datetime(entry)?; - Ok(self.0.set_entry(timestamp, ops.as_mut())?) + pub fn set_entry( + &mut self, + entry: Option>, + ops: &mut Operations, + ) -> anyhow::Result<()> { + Ok(self.0.set_entry(entry, ops.as_mut())?) } #[pyo3(signature=(wait, ops))] - pub fn set_wait(&mut self, wait: Option, ops: &mut Operations) -> anyhow::Result<()> { - let timestamp = Self::to_datetime(wait)?; - Ok(self.0.set_wait(timestamp, ops.as_mut())?) + pub fn set_wait( + &mut self, + wait: Option>, + ops: &mut Operations, + ) -> anyhow::Result<()> { + Ok(self.0.set_wait(wait, ops.as_mut())?) } #[pyo3(signature=(modified, ops))] - pub fn set_modified(&mut self, modified: String, ops: &mut Operations) -> anyhow::Result<()> { - let timestamp = DateTime::parse_from_rfc3339(&modified)?.with_timezone(&chrono::Utc); - Ok(self.0.set_modified(timestamp, ops.as_mut())?) + pub fn set_modified( + &mut self, + modified: DateTime, + ops: &mut Operations, + ) -> anyhow::Result<()> { + Ok(self.0.set_modified(modified, ops.as_mut())?) } #[pyo3(signature=(property, value, ops))] @@ -245,13 +257,15 @@ impl Task { Ok(self.0.remove_tag(tag.as_ref(), ops.as_mut())?) } - pub fn add_annotation(&mut self, ann: &Annotation, ops: &mut Operations) -> anyhow::Result<()> { - // Create an owned annotation - let mut annotation = Annotation::new(); - annotation.set_entry(ann.entry()); - annotation.set_description(ann.description()); - - Ok(self.0.add_annotation(annotation.0, ops.as_mut())?) + pub fn add_annotation( + &mut self, + annotation: &Annotation, + ops: &mut Operations, + ) -> anyhow::Result<()> { + // Create an owned annotation (TODO: not needed once + // https://github.com/GothenburgBitFactory/taskchampion/pull/517 is available) + let annotation = Annotation::new(annotation.entry(), annotation.description()); + Ok(self.0.add_annotation(annotation.into(), ops.as_mut())?) } pub fn remove_annotation( @@ -313,3 +327,21 @@ impl Task { Ok(self.0.remove_dependency(dep_uuid, ops.as_mut())?) } } + +impl AsRef for Task { + fn as_ref(&self) -> &TCTask { + &self.0 + } +} + +impl From for Task { + fn from(value: TCTask) -> Self { + Task(value) + } +} + +impl From for TCTask { + fn from(value: Task) -> Self { + value.0 + } +} diff --git a/taskchampion.pyi b/taskchampion.pyi index b0bb4f3..210b60f 100644 --- a/taskchampion.pyi +++ b/taskchampion.pyi @@ -101,9 +101,9 @@ class Task: def set_status(self, status: "Status", ops: "Operations"): ... def set_description(self, description: str, ops: "Operations"): ... def set_priority(self, priority: str, ops: "Operations"): ... - def set_entry(self, entry: Optional[str], ops: "Operations"): ... - def set_wait(self, wait: Optional[str], ops: "Operations"): ... - def set_modified(self, modified: Optional[str], ops: "Operations"): ... + def set_entry(self, entry: Optional[datetime], ops: "Operations"): ... + def set_wait(self, wait: Optional[datetime], ops: "Operations"): ... + def set_modified(self, modified: datetime, ops: "Operations"): ... def set_value(self, property: str, value: Optional[str], ops: "Operations"): ... def start(self, ops: "Operations"): ... def stop(self, ops: "Operations"): ... @@ -130,10 +130,10 @@ class WorkingSet: def __next__(self) -> tuple[int, str]: ... class Annotation: - entry: str + entry: datetime description: str - def __init__(self) -> None: ... + def __init__(self, entry: datetime, description: str) -> "Annotation": ... class DependencyMap: def dependencies(self, dep_of: str) -> list[str]: ... diff --git a/tests/test_annotation.py b/tests/test_annotation.py index 019f90d..da94d37 100644 --- a/tests/test_annotation.py +++ b/tests/test_annotation.py @@ -1,18 +1,28 @@ +import pytest +from datetime import datetime from taskchampion import Annotation -# IDK if this is a good idea, but it seems overkill to have another test to -# test the getter ... while using it for testing -def test_get_set_entry(): - a = Annotation() - a.entry = "2024-05-07T01:35:57+03:00" +@pytest.fixture +def entry() -> datetime: + return datetime.fromisoformat("2024-05-07T01:35:57+00:00") - assert a.entry == "2024-05-06T22:35:57+00:00" +@pytest.fixture +def annotation(entry) -> Annotation: + return Annotation(entry, "descr") -def test_get_set_description(): - a = Annotation() - a.description = "This is a basic description" +def test_entry(entry, annotation): + assert annotation.entry == entry - assert a.description == "This is a basic description" + +def test_repr(entry, annotation): + assert ( + repr(annotation) + == 'Annotation { entry: 2024-05-07T01:35:57Z, description: "descr" }' + ) + + +def test_description(annotation): + assert annotation.description == "descr" diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 0000000..27dfbd6 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,86 @@ +import re +from taskchampion import Replica, TaskData, Operations +from datetime import datetime +import pytest +import uuid + + +@pytest.fixture +def replica() -> Replica: + return Replica.new_in_memory() + + +@pytest.fixture +def new_task_uuid() -> str: + return str(uuid.uuid4()) + + +@pytest.fixture +def new_task_data(replica: Replica, new_task_uuid: str) -> TaskData: + ops = Operations() + task = TaskData.create(new_task_uuid, ops) + replica.commit_operations(ops) + return task + + +@pytest.fixture +def recurring_task_uuid() -> str: + return str(uuid.uuid4()) + + +@pytest.fixture +def recurring_task_data(replica: Replica, recurring_task_uuid: str) -> TaskData: + ops = Operations() + task = TaskData.create(recurring_task_uuid, ops) + task.update("status", "recurring", ops) + replica.commit_operations(ops) + return task + + +def test_taskdata_repr(new_task_data: TaskData, new_task_uuid: str): + assert repr(new_task_data) == f"TaskData {{ uuid: {new_task_uuid}, taskmap: {{}} }}" + + +def test_taskdata_get_uuid(new_task_data: TaskData, new_task_uuid: str): + assert new_task_data.get_uuid() == new_task_uuid + + +def test_taskdata_get(recurring_task_data: TaskData): + assert recurring_task_data.get("status") == "recurring" + + +def test_taskdata_get_not_set(new_task_data: TaskData): + assert new_task_data.get("status") == None + + +def test_taskdata_has(recurring_task_data: TaskData): + assert recurring_task_data.has("status") + + +def test_taskdata_has_not_set(new_task_data: TaskData): + assert not new_task_data.has("status") + + +def test_taskdata_update(replica: Replica, recurring_task_data: TaskData): + ops = Operations() + recurring_task_data.update("status", "pending", ops) + replica.commit_operations(ops) + + assert recurring_task_data.get("status") == "pending" + + +def test_taskdata_update_none(replica: Replica, recurring_task_data: TaskData): + ops = Operations() + recurring_task_data.update("status", None, ops) + replica.commit_operations(ops) + + assert recurring_task_data.get("status") == None + + +def test_taskdata_delete(replica: Replica, new_task_data: TaskData, new_task_uuid: str): + ops = Operations() + new_task_data.delete(ops) + replica.commit_operations(ops) + + deleted_task = replica.get_task(new_task_uuid) + assert deleted_task == None diff --git a/tests/test_task.py b/tests/test_task.py index 0c0579c..dd46e09 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -1,139 +1,420 @@ -from taskchampion import Task, Replica, Status, Tag, Operations +import re +from taskchampion import Task, Replica, Status, Tag, Operations, Annotation from datetime import datetime import pytest import uuid @pytest.fixture -def new_task(): - r = Replica.new_in_memory() - ops = Operations() - task = r.create_task(str(uuid.uuid4()), ops) - r.commit_operations(ops) +def replica() -> Replica: + return Replica.new_in_memory() + + +@pytest.fixture +def new_task_uuid() -> str: + return str(uuid.uuid4()) + +@pytest.fixture +def new_task(replica: Replica, new_task_uuid: str) -> Task: + ops = Operations() + task = replica.create_task(new_task_uuid, ops) + task.set_description("a task", ops) + task.set_status(Status.Pending, ops) + replica.commit_operations(ops) return task @pytest.fixture -def waiting_task(): - r = Replica.new_in_memory() +def waiting_task_uuid() -> str: + return str(uuid.uuid4()) + + +@pytest.fixture +def waiting_task(replica: Replica, waiting_task_uuid: str) -> Task: ops = Operations() - task = r.create_task(str(uuid.uuid4()), ops) - task.set_wait("2038-01-19T03:14:07+00:00", ops) + task = replica.create_task(waiting_task_uuid, ops) + task.set_description("a task", ops) + task.set_status(Status.Pending, ops) + task.set_wait(datetime.fromisoformat("2038-01-19T03:14:07+00:00"), ops) task.set_priority("10", ops) task.add_tag(Tag("example_tag"), ops) - r.commit_operations(ops) - + replica.commit_operations(ops) return task @pytest.fixture -def started_task(): - r = Replica.new_in_memory() +def started_task_uuid() -> str: + return str(uuid.uuid4()) + + +@pytest.fixture +def started_task(replica: Replica, started_task_uuid: str) -> Task: ops = Operations() - task = r.create_task(str(uuid.uuid4()), ops) + task = replica.create_task(started_task_uuid, ops) + task.set_description("a task", ops) + task.set_status(Status.Pending, ops) task.start(ops) - r.commit_operations(ops) - + replica.commit_operations(ops) return task @pytest.fixture -def blocked_task(): - r = Replica.new_in_memory() - ops = Operations() - task = r.create_task(str(uuid.uuid4()), ops) - r.commit_operations(ops) +def blocked_task_uuid() -> str: + return str(uuid.uuid4()) + +@pytest.fixture +def blocked_task(replica: Replica, started_task: Task, blocked_task_uuid: str) -> Task: + "Create a task blocked on started_task" + ops = Operations() + task = replica.create_task(blocked_task_uuid, ops) + task.set_description("a task", ops) + task.set_status(Status.Pending, ops) + task.add_dependency(started_task.get_uuid(), ops) + replica.commit_operations(ops) return task @pytest.fixture -def due_task(): - r = Replica.new_in_memory() +def due_task_uuid() -> str: + return str(uuid.uuid4()) + + +@pytest.fixture +def due_task(replica: Replica, due_task_uuid: str) -> Task: ops = Operations() - task = r.create_task(str(uuid.uuid4()), ops) + task = replica.create_task(due_task_uuid, ops) + task.set_description("a task", ops) + task.set_status(Status.Pending, ops) task.set_due(datetime.fromisoformat("2006-05-13T01:27:27+00:00"), ops) - r.commit_operations(ops) + replica.commit_operations(ops) + return task + + +@pytest.fixture +def annotated_task_uuid() -> str: + return str(uuid.uuid4()) + + +@pytest.fixture +def annotation_entry() -> datetime: + return datetime.fromisoformat("2010-05-13T01:27:27+00:00") + +@pytest.fixture +def annotated_task( + replica: Replica, annotated_task_uuid: str, annotation_entry: datetime +): + ops = Operations() + task = replica.create_task(annotated_task_uuid, ops) + task.set_description("a task", ops) + task.set_status(Status.Pending, ops) + task.add_annotation(Annotation(annotation_entry, "a thing happened"), ops) + replica.commit_operations(ops) return task @pytest.fixture -def task_with_description(): - r = Replica.new_in_memory() +def uda_task_uuid() -> str: + return str(uuid.uuid4()) + + +@pytest.fixture +def uda_task(replica: Replica, uda_task_uuid: str): ops = Operations() - task = r.create_task(str(uuid.uuid4()), ops) + task = replica.create_task(uda_task_uuid, ops) + task.set_description("a task", ops) + task.set_status(Status.Pending, ops) + task.set_uda("ns", "key", "val", ops) + replica.commit_operations(ops) + return task - task.set_description("This is a description", ops) - r.commit_operations(ops) +@pytest.fixture +def legacy_uda_task_uuid() -> str: + return str(uuid.uuid4()) + + +@pytest.fixture +def legacy_uda_task(replica: Replica, legacy_uda_task_uuid: str): + ops = Operations() + task = replica.create_task(legacy_uda_task_uuid, ops) + task.set_description("a task", ops) + task.set_status(Status.Pending, ops) + task.set_legacy_uda("legacy-key", "legacy-val", ops) + replica.commit_operations(ops) return task -def test_get_uuid(new_task: Task): - task_uuid = new_task.get_uuid() - assert uuid is not None +def test_repr(new_task: Task): + # The Rust Debug output contains lots of internal details that we do not + # need to check for here. + assert re.match(r"^Task { .* }$", repr(new_task)) + + +def test_into_task_data(new_task: Task, new_task_uuid: str): + new_task = new_task.into_task_data() + assert new_task.get_uuid() == new_task_uuid + - # This tests that the UUID is valid, it raises exception if not - uuid.UUID(task_uuid) +def test_get_uuid(new_task: Task, new_task_uuid: str): + assert new_task.get_uuid() == new_task_uuid -@pytest.mark.skip("This could be a bug") -def test_get_status(new_task: Task): +def test_get_set_status(new_task: Task): status = new_task.get_status() + assert status == Status.Pending - # for whatever reason these are not equivalent - # TODO: research if this is a bug - assert status is Status.Pending +def test_get_set_description(new_task: Task): + assert new_task.get_description() == "a task" -def test_get_priority(waiting_task: Task): + +def test_get_set_entry(replica: Replica, new_task: Task): + entry = datetime.fromisoformat("2038-01-19T03:14:07+00:00") + ops = Operations() + new_task.set_entry(entry, ops) + replica.commit_operations(ops) + new_task = replica.get_task(new_task.get_uuid()) + assert new_task.get_entry() == entry + + +def test_get_set_entry_none(replica: Replica, new_task: Task): + ops = Operations() + new_task.set_entry(None, ops) + replica.commit_operations(ops) + new_task = replica.get_task(new_task.get_uuid()) + assert new_task.get_entry() == None + + +def test_get_set_priority(waiting_task: Task): priority = waiting_task.get_priority() assert priority == "10" +def test_get_priority_missing(new_task: Task): + priority = new_task.get_priority() + assert priority == "" + + def test_get_wait(waiting_task: Task): assert waiting_task.get_wait() == datetime.fromisoformat( "2038-01-19T03:14:07+00:00" ) -def test_is_waiting(waiting_task: Task): +def test_is_waiting(new_task: Task, waiting_task: Task): + assert not new_task.is_waiting() assert waiting_task.is_waiting() -def test_is_active(started_task: Task): +def test_is_active(new_task: Task, started_task: Task): + assert not new_task.is_active() assert started_task.is_active() -@pytest.mark.skip() -def test_is_blocked(started_task: Task): - assert started_task.is_blocked() +def test_is_blocked(replica: Replica, new_task: Task, blocked_task: Task): + # Re-fetch tasks to get updated dependency map. + new_task = replica.get_task(new_task.get_uuid()) + blocked_task = replica.get_task(blocked_task.get_uuid()) + assert not new_task.is_blocked() + assert blocked_task.is_blocked() -@pytest.mark.skip() -def test_is_blocking(started_task: Task): +def test_is_blocking(replica: Replica, blocked_task: Task, started_task: Task): + # Re-fetch tasks to get updated dependency map. + blocked_task = replica.get_task(blocked_task.get_uuid()) + started_task = replica.get_task(started_task.get_uuid()) + assert not blocked_task.is_blocking() assert started_task.is_blocking() -@pytest.mark.skip("Enable this when able to add tags to the tasks") -def test_has_tag(waiting_task: Task): - assert waiting_task.has_tag(Tag("sample_tag")) +def test_has_tag_none(new_task: Task): + assert not new_task.has_tag(Tag("sample_tag")) + + +def test_has_tag_synthetic(replica: Replica, started_task: Task): + assert started_task.has_tag(Tag("PENDING")) + + +def test_has_tag_user(replica: Replica, new_task: Task): + ops = Operations() + new_task.add_tag(Tag("foo"), ops) + replica.commit_operations(ops) + assert new_task.has_tag(Tag("foo")) + + +def test_get_tags(replica: Replica, new_task: Task): + ops = Operations() + new_task.add_tag(Tag("foo"), ops) + replica.commit_operations(ops) + tags = new_task.get_tags() + # TaskChampion may add synthetic tags, so just assert a few we expect. + assert Tag("foo") in tags + assert Tag("PENDING") in tags + + +def test_remove_tag(replica: Replica, waiting_task: Task): + assert Tag("example_tag") in waiting_task.get_tags() + + ops = Operations() + waiting_task.remove_tag(Tag("example_tag"), ops) + replica.commit_operations(ops) + + assert Tag("example_tag") not in waiting_task.get_tags() + + +def test_get_annotations( + replica: Replica, annotated_task: Task, annotation_entry: datetime +): + annotations = annotated_task.get_annotations() + assert len(annotations) == 1 + assert annotations[0].entry == annotation_entry + assert annotations[0].description == "a thing happened" + + +def test_remove_annotation( + replica: Replica, annotated_task: Task, annotation_entry: datetime +): + ops = Operations() + annotated_task.remove_annotation(annotation_entry, ops) + replica.commit_operations(ops) + + annotations = annotated_task.get_annotations() + assert len(annotations) == 0 + + +def test_get_udas(uda_task: Task): + [((ns, key), val)] = uda_task.get_udas() + assert ns == "ns" + assert key == "key" + assert val == "val" + + +def test_get_udas_legacy(legacy_uda_task: Task): + [((ns, key), val)] = legacy_uda_task.get_udas() + assert ns == "" + assert key == "legacy-key" + assert val == "legacy-val" + + +def test_get_udas_none(new_task: Task): + [] = new_task.get_udas() + + +def test_remove_uda(replica: Replica, uda_task: Task): + ops = Operations() + uda_task.remove_uda("ns", "key", ops) + replica.commit_operations(ops) + uda_task = replica.get_task(uda_task.get_uuid()) + [] = uda_task.get_udas() + + +def test_remove_uda_no_such(replica: Replica, uda_task: Task): + ops = Operations() + uda_task.remove_uda("no", "such", ops) + replica.commit_operations(ops) + uda_task = replica.get_task(uda_task.get_uuid()) + assert len(uda_task.get_udas()) == 1 + + +def test_remove_legacy_uda(replica: Replica, legacy_uda_task: Task): + ops = Operations() + legacy_uda_task.remove_legacy_uda("legacy-key", ops) + replica.commit_operations(ops) + legacy_uda_task = replica.get_task(legacy_uda_task.get_uuid()) + [] = legacy_uda_task.get_udas() -@pytest.mark.skip("Enable this when able to add tags to the tasks") -def test_get_tags(waiting_task: Task): - assert waiting_task.get_tags() +def test_get_modified(replica: Replica, new_task: Task): + ops = Operations() + mod = datetime.fromisoformat("2006-05-13T01:27:27+00:00") + new_task.set_modified(mod, ops) + replica.commit_operations(ops) + assert new_task.get_modified() == mod -def test_get_modified(waiting_task: Task): - assert waiting_task.get_modified() is not None +def test_get_modified_not_set(replica: Replica, new_task_uuid: Task): + ops = Operations() + task = replica.create_task(new_task_uuid, ops) + replica.commit_operations(ops) + assert task.get_modified() == None def test_get_due(due_task: Task): assert due_task.get_due() == datetime.fromisoformat("2006-05-13T01:27:27+00:00") -def test_get_description(task_with_description): - assert task_with_description.get_description() == "This is a description" +def test_get_due_not_set(new_task: Task): + assert new_task.get_due() == None + + +def test_set_due(replica: Replica, new_task: Task): + due = datetime.fromisoformat("2006-05-13T01:27:27+00:00") + ops = Operations() + new_task.set_due(due, ops) + replica.commit_operations(ops) + assert new_task.get_due() == due + + +def test_set_due_none(replica: Replica, new_task: Task): + ops = Operations() + new_task.set_due(None, ops) + replica.commit_operations(ops) + assert new_task.get_due() == None + + +def test_get_dependencies( + blocked_task: Task, started_task: Task, started_task_uuid: str +): + assert started_task.get_dependencies() == [] + assert blocked_task.get_dependencies() == [started_task_uuid] + + +def test_remove_dependencies( + replica: Replica, + blocked_task: Task, + started_task: Task, + started_task_uuid: str, + new_task_uuid: str, +): + ops = Operations() + blocked_task.remove_dependency(started_task_uuid, ops) + blocked_task.remove_dependency( + new_task_uuid, ops + ) # Doesn't exist, and doesn't fail. + replica.commit_operations(ops) + assert blocked_task.get_dependencies() == [] + + +def test_get_value(new_task: Task): + assert new_task.get_value("status") == "pending" + + +def test_get_value_not_set(new_task: Task): + assert new_task.get_value("nosuchthing") == None + + +def test_start_stop(replica: Replica, new_task: Task): + assert not new_task.is_active() + + ops = Operations() + new_task.start(ops) + replica.commit_operations(ops) + + assert new_task.is_active() + + ops = Operations() + new_task.stop(ops) + replica.commit_operations(ops) + + assert not new_task.is_active() + + +def test_done(replica: Replica, new_task: Task): + ops = Operations() + new_task.done(ops) + replica.commit_operations(ops) + + assert new_task.get_status() == Status.Completed