X-Git-Url: https://git.madduck.net/etc/taskwarrior.git/blobdiff_plain/39ac0a190486d08dcba1c3301581e8129a2fca26..628b2d244b841be6494b87a14c3f78f0aa1cfc0e:/tasklib/tests.py diff --git a/tasklib/tests.py b/tasklib/tests.py index 4548df9..2aebdcb 100644 --- a/tasklib/tests.py +++ b/tasklib/tests.py @@ -1,12 +1,39 @@ # coding=utf-8 import datetime +import itertools +import json +import pytz +import six import shutil import tempfile import unittest -from .task import TaskWarrior, Task - +from .task import TaskWarrior, Task, local_zone, DATE_FORMAT + +# http://taskwarrior.org/docs/design/task.html , Section: The Attributes +TASK_STANDARD_ATTRS = ( + 'status', + 'uuid', + 'entry', + 'description', + 'start', + 'end', + 'due', + 'until', + 'wait', + 'modified', + 'scheduled', + 'recur', + 'mask', + 'imask', + 'parent', + 'project', + 'priority', + 'depends', + 'tags', + 'annotations', +) class TasklibTest(unittest.TestCase): @@ -70,6 +97,36 @@ class TaskFilterTest(TasklibTest): no_priority_task = self.tw.tasks.get(priority=None) self.assertEqual(no_priority_task['description'], "no priority task") + def test_filter_for_task_with_space_in_descripition(self): + task = Task(self.tw, description="test task") + task.save() + + filtered_task = self.tw.tasks.get(description="test task") + self.assertEqual(filtered_task['description'], "test task") + + def test_filter_for_task_without_space_in_descripition(self): + task = Task(self.tw, description="test") + task.save() + + filtered_task = self.tw.tasks.get(description="test") + self.assertEqual(filtered_task['description'], "test") + + def test_filter_for_task_with_space_in_project(self): + task = Task(self.tw, description="test", project="random project") + task.save() + + filtered_task = self.tw.tasks.get(project="random project") + self.assertEqual(filtered_task['project'], "random project") + + def test_filter_for_task_without_space_in_project(self): + task = Task(self.tw, description="test", project="random") + task.save() + + filtered_task = self.tw.tasks.get(project="random") + self.assertEqual(filtered_task['project'], "random") + + def test_filter_with_empty_uuid(self): + self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid='')) class TaskTest(TasklibTest): @@ -153,9 +210,9 @@ class TaskTest(TasklibTest): # We only save the parent task, dependency task is unsaved t.save() + t['depends'] = set([dependency]) - self.assertRaises(Task.NotSaved, - t.__setitem__, 'depends', set([dependency])) + self.assertRaises(Task.NotSaved, t.save) def test_set_simple_dependency_set(self): # Adds only one dependency to task with no dependencies @@ -195,7 +252,7 @@ class TaskTest(TasklibTest): t['depends'] = set([dependency1, dependency2]) t.save() - t['depends'] = t['depends'] - set([dependency2]) + t['depends'].remove(dependency2) t.save() self.assertEqual(t['depends'], set([dependency1])) @@ -212,11 +269,23 @@ class TaskTest(TasklibTest): t['depends'] = set([dependency1]) t.save() - t['depends'] = t['depends'] | set([dependency2]) + t['depends'].add(dependency2) t.save() self.assertEqual(t['depends'], set([dependency1, dependency2])) + def test_add_to_empty_dependency_set(self): + # Adds dependency to task with one dependencies + t = Task(self.tw, description="test task") + dependency = Task(self.tw, description="needs to be done first") + + dependency.save() + + t['depends'].add(dependency) + t.save() + + self.assertEqual(t['depends'], set([dependency])) + def test_simple_dependency_set_save_repeatedly(self): # Adds only one dependency to task with no dependencies t = Task(self.tw, description="test task") @@ -371,6 +440,14 @@ class TaskTest(TasklibTest): t['depends'] = set([dependency]) self.assertEqual(set(t._modified_fields), set()) + def test_modified_fields_not_affected_by_reading(self): + t = Task(self.tw) + + for field in TASK_STANDARD_ATTRS: + value = t[field] + + self.assertEqual(set(t._modified_fields), set()) + def test_setting_read_only_attrs_through_init(self): # Test that we are unable to set readonly attrs through __init__ for readonly_key in Task.read_only_fields: @@ -385,6 +462,182 @@ class TaskTest(TasklibTest): self.assertRaises(RuntimeError, lambda: t.__setitem__(readonly_key, 'value')) + def test_saving_unmodified_task(self): + t = Task(self.tw, description="test task") + t.save() + t.save() + + def test_adding_tag_by_appending(self): + t = Task(self.tw, description="test task", tags=['test1']) + t.save() + t['tags'].append('test2') + t.save() + self.assertEqual(t['tags'], ['test1', 'test2']) + + def test_adding_tag_by_appending_empty(self): + t = Task(self.tw, description="test task") + t.save() + t['tags'].append('test') + t.save() + self.assertEqual(t['tags'], ['test']) + + def test_serializers_returning_empty_string_for_none(self): + # Test that any serializer returns '' when passed None + t = Task(self.tw) + serializers = [getattr(t, serializer_name) for serializer_name in + filter(lambda x: x.startswith('serialize_'), dir(t))] + for serializer in serializers: + self.assertEqual(serializer(None), '') + + def test_deserializer_returning_empty_value_for_empty_string(self): + # Test that any deserializer returns empty value when passed '' + t = Task(self.tw) + deserializers = [getattr(t, deserializer_name) for deserializer_name in + filter(lambda x: x.startswith('deserialize_'), dir(t))] + for deserializer in deserializers: + self.assertTrue(deserializer('') in (None, [], set())) + + def test_normalizers_handling_none(self): + # Test that any normalizer can handle None as a valid value + t = Task(self.tw) + + # These normalizers are not supposed to handle None + exempt_normalizers = ('normalize_uuid', ) + + normalizers = [getattr(t, normalizer_name) for normalizer_name in + filter(lambda x: x.startswith('normalize_'), dir(t)) + if normalizer_name not in exempt_normalizers] + + for normalizer in normalizers: + normalizer(None) + + def test_recurrent_task_generation(self): + today = datetime.date.today() + t = Task(self.tw, description="brush teeth", + due=today, recur="daily") + t.save() + self.assertEqual(len(self.tw.tasks.pending()), 2) + +class TaskFromHookTest(TasklibTest): + + input_add_data = six.StringIO( + '{"description":"Buy some milk",' + '"entry":"20141118T050231Z",' + '"status":"pending",' + '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}') + + input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' + + '{"description":"Buy some milk finally",' + '"entry":"20141118T050231Z",' + '"status":"completed",' + '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}') + + exported_raw_data = ( + '{"project":"Home",' + '"due":"20150101T232323Z",' + '"description":"test task"}') + + def test_setting_up_from_add_hook_input(self): + t = Task.from_input(input_file=self.input_add_data) + self.assertEqual(t['description'], "Buy some milk") + self.assertEqual(t.pending, True) + + def test_setting_up_from_modified_hook_input(self): + t = Task.from_input(input_file=self.input_modify_data, modify=True) + self.assertEqual(t['description'], "Buy some milk finally") + self.assertEqual(t.pending, False) + self.assertEqual(t.completed, True) + + self.assertEqual(t._original_data['status'], "pending") + self.assertEqual(t._original_data['description'], "Buy some milk") + self.assertEqual(set(t._modified_fields), + set(['status', 'description'])) + + def test_export_data(self): + t = Task(self.tw, description="test task", + project="Home", + due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23))) + + # Check that the output is a permutation of: + # {"project":"Home","description":"test task","due":"20150101232323Z"} + allowed_segments = self.exported_raw_data[1:-1].split(',') + allowed_output = [ + '{' + ','.join(segments) + '}' + for segments in itertools.permutations(allowed_segments) + ] + + self.assertTrue(any(t.export_data() == expected + for expected in allowed_output)) + +class TimezoneAwareDatetimeTest(TasklibTest): + + def setUp(self): + super(TimezoneAwareDatetimeTest, self).setUp() + self.zone = local_zone + self.localdate_naive = datetime.datetime(2015,2,2) + self.localtime_naive = datetime.datetime(2015,2,2,0,0,0) + self.localtime_aware = self.zone.localize(self.localtime_naive) + self.utctime_aware = self.localtime_aware.astimezone(pytz.utc) + + def test_timezone_naive_datetime_setitem(self): + t = Task(self.tw, description="test task") + t['due'] = self.localtime_naive + self.assertEqual(t['due'], self.localtime_aware) + + def test_timezone_naive_datetime_using_init(self): + t = Task(self.tw, description="test task", due=self.localtime_naive) + self.assertEqual(t['due'], self.localtime_aware) + + def test_filter_by_naive_datetime(self): + t = Task(self.tw, description="task1", due=self.localtime_naive) + t.save() + matching_tasks = self.tw.tasks.filter(due=self.localtime_naive) + self.assertEqual(len(matching_tasks), 1) + + def test_serialize_naive_datetime(self): + t = Task(self.tw, description="task1", due=self.localtime_naive) + self.assertEqual(json.loads(t.export_data())['due'], + self.utctime_aware.strftime(DATE_FORMAT)) + + def test_timezone_naive_date_setitem(self): + t = Task(self.tw, description="test task") + t['due'] = self.localdate_naive + self.assertEqual(t['due'], self.localtime_aware) + + def test_timezone_naive_date_using_init(self): + t = Task(self.tw, description="test task", due=self.localdate_naive) + self.assertEqual(t['due'], self.localtime_aware) + + def test_filter_by_naive_date(self): + t = Task(self.tw, description="task1", due=self.localdate_naive) + t.save() + matching_tasks = self.tw.tasks.filter(due=self.localdate_naive) + self.assertEqual(len(matching_tasks), 1) + + def test_serialize_naive_date(self): + t = Task(self.tw, description="task1", due=self.localdate_naive) + self.assertEqual(json.loads(t.export_data())['due'], + self.utctime_aware.strftime(DATE_FORMAT)) + + def test_timezone_aware_datetime_setitem(self): + t = Task(self.tw, description="test task") + t['due'] = self.localtime_aware + self.assertEqual(t['due'], self.localtime_aware) + + def test_timezone_aware_datetime_using_init(self): + t = Task(self.tw, description="test task", due=self.localtime_aware) + self.assertEqual(t['due'], self.localtime_aware) + + def test_filter_by_aware_datetime(self): + t = Task(self.tw, description="task1", due=self.localtime_aware) + t.save() + matching_tasks = self.tw.tasks.filter(due=self.localtime_aware) + self.assertEqual(len(matching_tasks), 1) + + def test_serialize_aware_datetime(self): + t = Task(self.tw, description="task1", due=self.localtime_aware) + self.assertEqual(json.loads(t.export_data())['due'], + self.utctime_aware.strftime(DATE_FORMAT)) class AnnotationTest(TasklibTest): @@ -419,6 +672,31 @@ class AnnotationTest(TasklibTest): task.remove_annotation(ann) self.assertEqual(len(task['annotations']), 0) + def test_annotation_after_modification(self): + task = self.tw.tasks.get() + task['project'] = 'test' + task.add_annotation('I should really do this task') + self.assertEqual(task['project'], 'test') + task.save() + self.assertEqual(task['project'], 'test') + + def test_serialize_annotations(self): + # Test that serializing annotations is possible + t = Task(self.tw, description="test") + t.save() + + t.add_annotation("annotation1") + t.add_annotation("annotation2") + + data = t._serialize('annotations', t._data['annotations']) + + self.assertEqual(len(data), 2) + self.assertEqual(type(data[0]), dict) + self.assertEqual(type(data[1]), dict) + + self.assertEqual(data[0]['description'], "annotation1") + self.assertEqual(data[1]['description'], "annotation2") + class UnicodeTest(TasklibTest):