All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
13 from .task import TaskWarrior, Task, ReadOnlyDictView, local_zone, DATE_FORMAT
15 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
16 TASK_STANDARD_ATTRS = (
39 class TasklibTest(unittest.TestCase):
42 self.tmp = tempfile.mkdtemp(dir='.')
43 self.tw = TaskWarrior(data_location=self.tmp)
46 shutil.rmtree(self.tmp)
49 class TaskFilterTest(TasklibTest):
51 def test_all_empty(self):
52 self.assertEqual(len(self.tw.tasks.all()), 0)
54 def test_all_non_empty(self):
55 Task(self.tw, description="test task").save()
56 self.assertEqual(len(self.tw.tasks.all()), 1)
57 self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
58 self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
60 def test_pending_non_empty(self):
61 Task(self.tw, description="test task").save()
62 self.assertEqual(len(self.tw.tasks.pending()), 1)
63 self.assertEqual(self.tw.tasks.pending()[0]['description'],
65 self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
67 def test_completed_empty(self):
68 Task(self.tw, description="test task").save()
69 self.assertEqual(len(self.tw.tasks.completed()), 0)
71 def test_completed_non_empty(self):
72 Task(self.tw, description="test task").save()
73 self.assertEqual(len(self.tw.tasks.completed()), 0)
74 self.tw.tasks.all()[0].done()
75 self.assertEqual(len(self.tw.tasks.completed()), 1)
77 def test_filtering_by_attribute(self):
78 Task(self.tw, description="no priority task").save()
79 Task(self.tw, priority="H", description="high priority task").save()
80 self.assertEqual(len(self.tw.tasks.all()), 2)
82 # Assert that the correct number of tasks is returned
83 self.assertEqual(len(self.tw.tasks.filter(priority="H")), 1)
85 # Assert that the correct tasks are returned
86 high_priority_task = self.tw.tasks.get(priority="H")
87 self.assertEqual(high_priority_task['description'], "high priority task")
89 def test_filtering_by_empty_attribute(self):
90 Task(self.tw, description="no priority task").save()
91 Task(self.tw, priority="H", description="high priority task").save()
92 self.assertEqual(len(self.tw.tasks.all()), 2)
94 # Assert that the correct number of tasks is returned
95 self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
97 # Assert that the correct tasks are returned
98 no_priority_task = self.tw.tasks.get(priority=None)
99 self.assertEqual(no_priority_task['description'], "no priority task")
101 def test_filter_for_task_with_space_in_descripition(self):
102 task = Task(self.tw, description="test task")
105 filtered_task = self.tw.tasks.get(description="test task")
106 self.assertEqual(filtered_task['description'], "test task")
108 def test_filter_for_task_without_space_in_descripition(self):
109 task = Task(self.tw, description="test")
112 filtered_task = self.tw.tasks.get(description="test")
113 self.assertEqual(filtered_task['description'], "test")
115 def test_filter_for_task_with_space_in_project(self):
116 task = Task(self.tw, description="test", project="random project")
119 filtered_task = self.tw.tasks.get(project="random project")
120 self.assertEqual(filtered_task['project'], "random project")
122 def test_filter_for_task_without_space_in_project(self):
123 task = Task(self.tw, description="test", project="random")
126 filtered_task = self.tw.tasks.get(project="random")
127 self.assertEqual(filtered_task['project'], "random")
129 def test_filter_with_empty_uuid(self):
130 self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid=''))
132 class TaskTest(TasklibTest):
134 def test_create_unsaved_task(self):
135 # Make sure a new task is not saved unless explicitly called for
136 t = Task(self.tw, description="test task")
137 self.assertEqual(len(self.tw.tasks.all()), 0)
139 # TODO: once python 2.6 compatiblity is over, use context managers here
140 # and in all subsequent tests for assertRaises
142 def test_delete_unsaved_task(self):
143 t = Task(self.tw, description="test task")
144 self.assertRaises(Task.NotSaved, t.delete)
146 def test_complete_unsaved_task(self):
147 t = Task(self.tw, description="test task")
148 self.assertRaises(Task.NotSaved, t.done)
150 def test_refresh_unsaved_task(self):
151 t = Task(self.tw, description="test task")
152 self.assertRaises(Task.NotSaved, t.refresh)
154 def test_delete_deleted_task(self):
155 t = Task(self.tw, description="test task")
159 self.assertRaises(Task.DeletedTask, t.delete)
161 def test_complete_completed_task(self):
162 t = Task(self.tw, description="test task")
166 self.assertRaises(Task.CompletedTask, t.done)
168 def test_complete_deleted_task(self):
169 t = Task(self.tw, description="test task")
173 self.assertRaises(Task.DeletedTask, t.done)
175 def test_modify_simple_attribute_without_space(self):
176 t = Task(self.tw, description="test")
179 self.assertEquals(t['description'], "test")
181 t['description'] = "test-modified"
184 self.assertEquals(t['description'], "test-modified")
186 def test_modify_simple_attribute_with_space(self):
187 # Space can pose problems with parsing
188 t = Task(self.tw, description="test task")
191 self.assertEquals(t['description'], "test task")
193 t['description'] = "test task modified"
196 self.assertEquals(t['description'], "test task modified")
198 def test_empty_dependency_set_of_unsaved_task(self):
199 t = Task(self.tw, description="test task")
200 self.assertEqual(t['depends'], set())
202 def test_empty_dependency_set_of_saved_task(self):
203 t = Task(self.tw, description="test task")
205 self.assertEqual(t['depends'], set())
207 def test_set_unsaved_task_as_dependency(self):
208 # Adds only one dependency to task with no dependencies
209 t = Task(self.tw, description="test task")
210 dependency = Task(self.tw, description="needs to be done first")
212 # We only save the parent task, dependency task is unsaved
214 t['depends'] = set([dependency])
216 self.assertRaises(Task.NotSaved, t.save)
218 def test_set_simple_dependency_set(self):
219 # Adds only one dependency to task with no dependencies
220 t = Task(self.tw, description="test task")
221 dependency = Task(self.tw, description="needs to be done first")
226 t['depends'] = set([dependency])
228 self.assertEqual(t['depends'], set([dependency]))
230 def test_set_complex_dependency_set(self):
231 # Adds two dependencies to task with no dependencies
232 t = Task(self.tw, description="test task")
233 dependency1 = Task(self.tw, description="needs to be done first")
234 dependency2 = Task(self.tw, description="needs to be done second")
240 t['depends'] = set([dependency1, dependency2])
242 self.assertEqual(t['depends'], set([dependency1, dependency2]))
244 def test_remove_from_dependency_set(self):
245 # Removes dependency from task with two dependencies
246 t = Task(self.tw, description="test task")
247 dependency1 = Task(self.tw, description="needs to be done first")
248 dependency2 = Task(self.tw, description="needs to be done second")
253 t['depends'] = set([dependency1, dependency2])
256 t['depends'].remove(dependency2)
259 self.assertEqual(t['depends'], set([dependency1]))
261 def test_add_to_dependency_set(self):
262 # Adds dependency to task with one dependencies
263 t = Task(self.tw, description="test task")
264 dependency1 = Task(self.tw, description="needs to be done first")
265 dependency2 = Task(self.tw, description="needs to be done second")
270 t['depends'] = set([dependency1])
273 t['depends'].add(dependency2)
276 self.assertEqual(t['depends'], set([dependency1, dependency2]))
278 def test_add_to_empty_dependency_set(self):
279 # Adds dependency to task with one dependencies
280 t = Task(self.tw, description="test task")
281 dependency = Task(self.tw, description="needs to be done first")
285 t['depends'].add(dependency)
288 self.assertEqual(t['depends'], set([dependency]))
290 def test_simple_dependency_set_save_repeatedly(self):
291 # Adds only one dependency to task with no dependencies
292 t = Task(self.tw, description="test task")
293 dependency = Task(self.tw, description="needs to be done first")
296 t['depends'] = set([dependency])
299 # We taint the task, but keep depends intact
300 t['description'] = "test task modified"
303 self.assertEqual(t['depends'], set([dependency]))
305 # We taint the task, but assign the same set to the depends
306 t['depends'] = set([dependency])
307 t['description'] = "test task modified again"
310 self.assertEqual(t['depends'], set([dependency]))
312 def test_compare_different_tasks(self):
313 # Negative: compare two different tasks
314 t1 = Task(self.tw, description="test task")
315 t2 = Task(self.tw, description="test task")
320 self.assertEqual(t1 == t2, False)
322 def test_compare_same_task_object(self):
323 # Compare Task object wit itself
324 t = Task(self.tw, description="test task")
327 self.assertEqual(t == t, True)
329 def test_compare_same_task(self):
330 # Compare the same task using two different objects
331 t1 = Task(self.tw, description="test task")
334 t2 = self.tw.tasks.get(uuid=t1['uuid'])
335 self.assertEqual(t1 == t2, True)
337 def test_compare_unsaved_tasks(self):
338 # t1 and t2 are unsaved tasks, considered to be unequal
339 # despite the content of data
340 t1 = Task(self.tw, description="test task")
341 t2 = Task(self.tw, description="test task")
343 self.assertEqual(t1 == t2, False)
345 def test_hash_unsaved_tasks(self):
346 # Considered equal, it's the same object
347 t1 = Task(self.tw, description="test task")
349 self.assertEqual(hash(t1) == hash(t2), True)
351 def test_hash_same_task(self):
352 # Compare the hash of the task using two different objects
353 t1 = Task(self.tw, description="test task")
356 t2 = self.tw.tasks.get(uuid=t1['uuid'])
357 self.assertEqual(t1.__hash__(), t2.__hash__())
359 def test_adding_task_with_priority(self):
360 t = Task(self.tw, description="test task", priority="M")
363 def test_removing_priority_with_none(self):
364 t = Task(self.tw, description="test task", priority="L")
367 # Remove the priority mark
371 # Assert that priority is not there after saving
372 self.assertEqual(t['priority'], None)
374 def test_adding_task_with_due_time(self):
375 t = Task(self.tw, description="test task", due=datetime.datetime.now())
378 def test_removing_due_time_with_none(self):
379 t = Task(self.tw, description="test task", due=datetime.datetime.now())
382 # Remove the due timestamp
386 # Assert that due timestamp is no longer there
387 self.assertEqual(t['due'], None)
389 def test_modified_fields_new_task(self):
392 # This should be empty with new task
393 self.assertEqual(set(t._modified_fields), set())
396 t['description'] = "test task"
397 self.assertEqual(set(t._modified_fields), set(['description']))
399 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
400 self.assertEqual(set(t._modified_fields), set(['description', 'due']))
402 t['project'] = "test project"
403 self.assertEqual(set(t._modified_fields),
404 set(['description', 'due', 'project']))
406 # List of modified fields should clear out when saved
408 self.assertEqual(set(t._modified_fields), set())
410 # Reassigning the fields with the same values now should not produce
412 t['description'] = "test task"
413 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
414 t['project'] = "test project"
415 self.assertEqual(set(t._modified_fields), set())
417 def test_modified_fields_loaded_task(self):
421 t['description'] = "test task"
422 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
423 t['project'] = "test project"
425 dependency = Task(self.tw, description="dependency")
427 t['depends'] = set([dependency])
429 # List of modified fields should clear out when saved
431 self.assertEqual(set(t._modified_fields), set())
433 # Get the task by using a filter by UUID
434 t2 = self.tw.tasks.get(uuid=t['uuid'])
436 # Reassigning the fields with the same values now should not produce
438 t['description'] = "test task"
439 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
440 t['project'] = "test project"
441 t['depends'] = set([dependency])
442 self.assertEqual(set(t._modified_fields), set())
444 def test_modified_fields_not_affected_by_reading(self):
447 for field in TASK_STANDARD_ATTRS:
450 self.assertEqual(set(t._modified_fields), set())
452 def test_setting_read_only_attrs_through_init(self):
453 # Test that we are unable to set readonly attrs through __init__
454 for readonly_key in Task.read_only_fields:
455 kwargs = {'description': 'test task', readonly_key: 'value'}
456 self.assertRaises(RuntimeError,
457 lambda: Task(self.tw, **kwargs))
459 def test_setting_read_only_attrs_through_setitem(self):
460 # Test that we are unable to set readonly attrs through __init__
461 for readonly_key in Task.read_only_fields:
462 t = Task(self.tw, description='test task')
463 self.assertRaises(RuntimeError,
464 lambda: t.__setitem__(readonly_key, 'value'))
466 def test_saving_unmodified_task(self):
467 t = Task(self.tw, description="test task")
471 def test_adding_tag_by_appending(self):
472 t = Task(self.tw, description="test task", tags=['test1'])
474 t['tags'].append('test2')
476 self.assertEqual(t['tags'], ['test1', 'test2'])
478 def test_adding_tag_by_appending_empty(self):
479 t = Task(self.tw, description="test task")
481 t['tags'].append('test')
483 self.assertEqual(t['tags'], ['test'])
485 def test_serializers_returning_empty_string_for_none(self):
486 # Test that any serializer returns '' when passed None
488 serializers = [getattr(t, serializer_name) for serializer_name in
489 filter(lambda x: x.startswith('serialize_'), dir(t))]
490 for serializer in serializers:
491 self.assertEqual(serializer(None), '')
493 def test_deserializer_returning_empty_value_for_empty_string(self):
494 # Test that any deserializer returns empty value when passed ''
496 deserializers = [getattr(t, deserializer_name) for deserializer_name in
497 filter(lambda x: x.startswith('deserialize_'), dir(t))]
498 for deserializer in deserializers:
499 self.assertTrue(deserializer('') in (None, [], set()))
501 def test_normalizers_handling_none(self):
502 # Test that any normalizer can handle None as a valid value
505 # These normalizers are not supposed to handle None
506 exempt_normalizers = ('normalize_uuid', )
508 normalizers = [getattr(t, normalizer_name) for normalizer_name in
509 filter(lambda x: x.startswith('normalize_'), dir(t))
510 if normalizer_name not in exempt_normalizers]
512 for normalizer in normalizers:
515 def test_recurrent_task_generation(self):
516 today = datetime.date.today()
517 t = Task(self.tw, description="brush teeth",
518 due=today, recur="daily")
520 self.assertEqual(len(self.tw.tasks.pending()), 2)
522 class TaskFromHookTest(TasklibTest):
524 input_add_data = six.StringIO(
525 '{"description":"Buy some milk",'
526 '"entry":"20141118T050231Z",'
527 '"status":"pending",'
528 '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
530 input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
531 '{"description":"Buy some milk finally",'
532 '"entry":"20141118T050231Z",'
533 '"status":"completed",'
534 '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
536 exported_raw_data = (
538 '"due":"20150101T232323Z",'
539 '"description":"test task"}')
541 def test_setting_up_from_add_hook_input(self):
542 t = Task.from_input(input_file=self.input_add_data)
543 self.assertEqual(t['description'], "Buy some milk")
544 self.assertEqual(t.pending, True)
546 def test_setting_up_from_modified_hook_input(self):
547 t = Task.from_input(input_file=self.input_modify_data, modify=True)
548 self.assertEqual(t['description'], "Buy some milk finally")
549 self.assertEqual(t.pending, False)
550 self.assertEqual(t.completed, True)
552 self.assertEqual(t._original_data['status'], "pending")
553 self.assertEqual(t._original_data['description'], "Buy some milk")
554 self.assertEqual(set(t._modified_fields),
555 set(['status', 'description']))
557 def test_export_data(self):
558 t = Task(self.tw, description="test task",
560 due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
562 # Check that the output is a permutation of:
563 # {"project":"Home","description":"test task","due":"20150101232323Z"}
564 allowed_segments = self.exported_raw_data[1:-1].split(',')
566 '{' + ','.join(segments) + '}'
567 for segments in itertools.permutations(allowed_segments)
570 self.assertTrue(any(t.export_data() == expected
571 for expected in allowed_output))
573 class TimezoneAwareDatetimeTest(TasklibTest):
576 super(TimezoneAwareDatetimeTest, self).setUp()
577 self.zone = local_zone
578 self.localdate_naive = datetime.datetime(2015,2,2)
579 self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
580 self.localtime_aware = self.zone.localize(self.localtime_naive)
581 self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
583 def test_timezone_naive_datetime_setitem(self):
584 t = Task(self.tw, description="test task")
585 t['due'] = self.localtime_naive
586 self.assertEqual(t['due'], self.localtime_aware)
588 def test_timezone_naive_datetime_using_init(self):
589 t = Task(self.tw, description="test task", due=self.localtime_naive)
590 self.assertEqual(t['due'], self.localtime_aware)
592 def test_filter_by_naive_datetime(self):
593 t = Task(self.tw, description="task1", due=self.localtime_naive)
595 matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
596 self.assertEqual(len(matching_tasks), 1)
598 def test_serialize_naive_datetime(self):
599 t = Task(self.tw, description="task1", due=self.localtime_naive)
600 self.assertEqual(json.loads(t.export_data())['due'],
601 self.utctime_aware.strftime(DATE_FORMAT))
603 def test_timezone_naive_date_setitem(self):
604 t = Task(self.tw, description="test task")
605 t['due'] = self.localdate_naive
606 self.assertEqual(t['due'], self.localtime_aware)
608 def test_timezone_naive_date_using_init(self):
609 t = Task(self.tw, description="test task", due=self.localdate_naive)
610 self.assertEqual(t['due'], self.localtime_aware)
612 def test_filter_by_naive_date(self):
613 t = Task(self.tw, description="task1", due=self.localdate_naive)
615 matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
616 self.assertEqual(len(matching_tasks), 1)
618 def test_serialize_naive_date(self):
619 t = Task(self.tw, description="task1", due=self.localdate_naive)
620 self.assertEqual(json.loads(t.export_data())['due'],
621 self.utctime_aware.strftime(DATE_FORMAT))
623 def test_timezone_aware_datetime_setitem(self):
624 t = Task(self.tw, description="test task")
625 t['due'] = self.localtime_aware
626 self.assertEqual(t['due'], self.localtime_aware)
628 def test_timezone_aware_datetime_using_init(self):
629 t = Task(self.tw, description="test task", due=self.localtime_aware)
630 self.assertEqual(t['due'], self.localtime_aware)
632 def test_filter_by_aware_datetime(self):
633 t = Task(self.tw, description="task1", due=self.localtime_aware)
635 matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
636 self.assertEqual(len(matching_tasks), 1)
638 def test_serialize_aware_datetime(self):
639 t = Task(self.tw, description="task1", due=self.localtime_aware)
640 self.assertEqual(json.loads(t.export_data())['due'],
641 self.utctime_aware.strftime(DATE_FORMAT))
643 class AnnotationTest(TasklibTest):
646 super(AnnotationTest, self).setUp()
647 Task(self.tw, description="test task").save()
649 def test_adding_annotation(self):
650 task = self.tw.tasks.get()
651 task.add_annotation('test annotation')
652 self.assertEqual(len(task['annotations']), 1)
653 ann = task['annotations'][0]
654 self.assertEqual(ann['description'], 'test annotation')
656 def test_removing_annotation(self):
657 task = self.tw.tasks.get()
658 task.add_annotation('test annotation')
659 ann = task['annotations'][0]
661 self.assertEqual(len(task['annotations']), 0)
663 def test_removing_annotation_by_description(self):
664 task = self.tw.tasks.get()
665 task.add_annotation('test annotation')
666 task.remove_annotation('test annotation')
667 self.assertEqual(len(task['annotations']), 0)
669 def test_removing_annotation_by_obj(self):
670 task = self.tw.tasks.get()
671 task.add_annotation('test annotation')
672 ann = task['annotations'][0]
673 task.remove_annotation(ann)
674 self.assertEqual(len(task['annotations']), 0)
676 def test_annotation_after_modification(self):
677 task = self.tw.tasks.get()
678 task['project'] = 'test'
679 task.add_annotation('I should really do this task')
680 self.assertEqual(task['project'], 'test')
682 self.assertEqual(task['project'], 'test')
684 def test_serialize_annotations(self):
685 # Test that serializing annotations is possible
686 t = Task(self.tw, description="test")
689 t.add_annotation("annotation1")
690 t.add_annotation("annotation2")
692 data = t._serialize('annotations', t._data['annotations'])
694 self.assertEqual(len(data), 2)
695 self.assertEqual(type(data[0]), dict)
696 self.assertEqual(type(data[1]), dict)
698 self.assertEqual(data[0]['description'], "annotation1")
699 self.assertEqual(data[1]['description'], "annotation2")
702 class UnicodeTest(TasklibTest):
704 def test_unicode_task(self):
705 Task(self.tw, description="†åßk").save()
708 def test_non_unicode_task(self):
709 Task(self.tw, description="test task").save()
712 class ReadOnlyDictViewTest(unittest.TestCase):
715 self.sample = dict(l=[1,2,3], d={'k':'v'})
716 self.original_sample = copy.deepcopy(self.sample)
717 self.view = ReadOnlyDictView(self.sample)
719 def test_readonlydictview_getitem(self):
721 self.assertEqual(l, self.sample['l'])
723 # Assert that modification changed only copied value
725 self.assertNotEqual(l, self.sample['l'])
727 # Assert that viewed dict is not changed
728 self.assertEqual(self.sample, self.original_sample)
730 def test_readonlydictview_contains(self):
731 self.assertEqual('l' in self.view, 'l' in self.sample)
732 self.assertEqual('d' in self.view, 'd' in self.sample)
733 self.assertEqual('k' in self.view, 'k' in self.sample)
735 # Assert that viewed dict is not changed
736 self.assertEqual(self.sample, self.original_sample)
738 def test_readonlydictview_iter(self):
739 self.assertEqual(list(k for k in self.view),
740 list(k for k in self.sample))
742 # Assert the view is correct after modification
743 self.sample['new'] = 'value'
744 self.assertEqual(list(k for k in self.view),
745 list(k for k in self.sample))
747 def test_readonlydictview_len(self):
748 self.assertEqual(len(self.view), len(self.sample))
750 # Assert the view is correct after modification
751 self.sample['new'] = 'value'
752 self.assertEqual(len(self.view), len(self.sample))
754 def test_readonlydictview_get(self):
755 l = self.view.get('l')
756 self.assertEqual(l, self.sample.get('l'))
758 # Assert that modification changed only copied value
760 self.assertNotEqual(l, self.sample.get('l'))
762 # Assert that viewed dict is not changed
763 self.assertEqual(self.sample, self.original_sample)
765 def test_readonlydict_items(self):
766 view_items = self.view.items()
767 sample_items = list(self.sample.items())
768 self.assertEqual(view_items, sample_items)
770 view_items.append('newkey')
771 self.assertNotEqual(view_items, sample_items)
772 self.assertEqual(self.sample, self.original_sample)
774 def test_readonlydict_values(self):
775 view_values = self.view.values()
776 sample_values = list(self.sample.values())
777 self.assertEqual(view_values, sample_values)
779 view_list_item = list(filter(lambda x: type(x) is list,
781 view_list_item.append(4)
782 self.assertNotEqual(view_values, sample_values)
783 self.assertEqual(self.sample, self.original_sample)