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.
12 from .task import TaskWarrior, Task, local_zone, DATE_FORMAT
14 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
15 TASK_STANDARD_ATTRS = (
38 class TasklibTest(unittest.TestCase):
41 self.tmp = tempfile.mkdtemp(dir='.')
42 self.tw = TaskWarrior(data_location=self.tmp)
45 shutil.rmtree(self.tmp)
48 class TaskFilterTest(TasklibTest):
50 def test_all_empty(self):
51 self.assertEqual(len(self.tw.tasks.all()), 0)
53 def test_all_non_empty(self):
54 Task(self.tw, description="test task").save()
55 self.assertEqual(len(self.tw.tasks.all()), 1)
56 self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
57 self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
59 def test_pending_non_empty(self):
60 Task(self.tw, description="test task").save()
61 self.assertEqual(len(self.tw.tasks.pending()), 1)
62 self.assertEqual(self.tw.tasks.pending()[0]['description'],
64 self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
66 def test_completed_empty(self):
67 Task(self.tw, description="test task").save()
68 self.assertEqual(len(self.tw.tasks.completed()), 0)
70 def test_completed_non_empty(self):
71 Task(self.tw, description="test task").save()
72 self.assertEqual(len(self.tw.tasks.completed()), 0)
73 self.tw.tasks.all()[0].done()
74 self.assertEqual(len(self.tw.tasks.completed()), 1)
76 def test_filtering_by_attribute(self):
77 Task(self.tw, description="no priority task").save()
78 Task(self.tw, priority="H", description="high priority task").save()
79 self.assertEqual(len(self.tw.tasks.all()), 2)
81 # Assert that the correct number of tasks is returned
82 self.assertEqual(len(self.tw.tasks.filter(priority="H")), 1)
84 # Assert that the correct tasks are returned
85 high_priority_task = self.tw.tasks.get(priority="H")
86 self.assertEqual(high_priority_task['description'], "high priority task")
88 def test_filtering_by_empty_attribute(self):
89 Task(self.tw, description="no priority task").save()
90 Task(self.tw, priority="H", description="high priority task").save()
91 self.assertEqual(len(self.tw.tasks.all()), 2)
93 # Assert that the correct number of tasks is returned
94 self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
96 # Assert that the correct tasks are returned
97 no_priority_task = self.tw.tasks.get(priority=None)
98 self.assertEqual(no_priority_task['description'], "no priority task")
100 def test_filter_for_task_with_space_in_descripition(self):
101 task = Task(self.tw, description="test task")
104 filtered_task = self.tw.tasks.get(description="test task")
105 self.assertEqual(filtered_task['description'], "test task")
107 def test_filter_for_task_without_space_in_descripition(self):
108 task = Task(self.tw, description="test")
111 filtered_task = self.tw.tasks.get(description="test")
112 self.assertEqual(filtered_task['description'], "test")
114 def test_filter_for_task_with_space_in_project(self):
115 task = Task(self.tw, description="test", project="random project")
118 filtered_task = self.tw.tasks.get(project="random project")
119 self.assertEqual(filtered_task['project'], "random project")
121 def test_filter_for_task_without_space_in_project(self):
122 task = Task(self.tw, description="test", project="random")
125 filtered_task = self.tw.tasks.get(project="random")
126 self.assertEqual(filtered_task['project'], "random")
128 def test_filter_with_empty_uuid(self):
129 self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid=''))
131 class TaskTest(TasklibTest):
133 def test_create_unsaved_task(self):
134 # Make sure a new task is not saved unless explicitly called for
135 t = Task(self.tw, description="test task")
136 self.assertEqual(len(self.tw.tasks.all()), 0)
138 # TODO: once python 2.6 compatiblity is over, use context managers here
139 # and in all subsequent tests for assertRaises
141 def test_delete_unsaved_task(self):
142 t = Task(self.tw, description="test task")
143 self.assertRaises(Task.NotSaved, t.delete)
145 def test_complete_unsaved_task(self):
146 t = Task(self.tw, description="test task")
147 self.assertRaises(Task.NotSaved, t.done)
149 def test_refresh_unsaved_task(self):
150 t = Task(self.tw, description="test task")
151 self.assertRaises(Task.NotSaved, t.refresh)
153 def test_delete_deleted_task(self):
154 t = Task(self.tw, description="test task")
158 self.assertRaises(Task.DeletedTask, t.delete)
160 def test_complete_completed_task(self):
161 t = Task(self.tw, description="test task")
165 self.assertRaises(Task.CompletedTask, t.done)
167 def test_complete_deleted_task(self):
168 t = Task(self.tw, description="test task")
172 self.assertRaises(Task.DeletedTask, t.done)
174 def test_modify_simple_attribute_without_space(self):
175 t = Task(self.tw, description="test")
178 self.assertEquals(t['description'], "test")
180 t['description'] = "test-modified"
183 self.assertEquals(t['description'], "test-modified")
185 def test_modify_simple_attribute_with_space(self):
186 # Space can pose problems with parsing
187 t = Task(self.tw, description="test task")
190 self.assertEquals(t['description'], "test task")
192 t['description'] = "test task modified"
195 self.assertEquals(t['description'], "test task modified")
197 def test_empty_dependency_set_of_unsaved_task(self):
198 t = Task(self.tw, description="test task")
199 self.assertEqual(t['depends'], set())
201 def test_empty_dependency_set_of_saved_task(self):
202 t = Task(self.tw, description="test task")
204 self.assertEqual(t['depends'], set())
206 def test_set_unsaved_task_as_dependency(self):
207 # Adds only one dependency to task with no dependencies
208 t = Task(self.tw, description="test task")
209 dependency = Task(self.tw, description="needs to be done first")
211 # We only save the parent task, dependency task is unsaved
213 t['depends'] = set([dependency])
215 self.assertRaises(Task.NotSaved, t.save)
217 def test_set_simple_dependency_set(self):
218 # Adds only one dependency to task with no dependencies
219 t = Task(self.tw, description="test task")
220 dependency = Task(self.tw, description="needs to be done first")
225 t['depends'] = set([dependency])
227 self.assertEqual(t['depends'], set([dependency]))
229 def test_set_complex_dependency_set(self):
230 # Adds two dependencies to task with no dependencies
231 t = Task(self.tw, description="test task")
232 dependency1 = Task(self.tw, description="needs to be done first")
233 dependency2 = Task(self.tw, description="needs to be done second")
239 t['depends'] = set([dependency1, dependency2])
241 self.assertEqual(t['depends'], set([dependency1, dependency2]))
243 def test_remove_from_dependency_set(self):
244 # Removes dependency from task with two dependencies
245 t = Task(self.tw, description="test task")
246 dependency1 = Task(self.tw, description="needs to be done first")
247 dependency2 = Task(self.tw, description="needs to be done second")
252 t['depends'] = set([dependency1, dependency2])
255 t['depends'].remove(dependency2)
258 self.assertEqual(t['depends'], set([dependency1]))
260 def test_add_to_dependency_set(self):
261 # Adds dependency to task with one dependencies
262 t = Task(self.tw, description="test task")
263 dependency1 = Task(self.tw, description="needs to be done first")
264 dependency2 = Task(self.tw, description="needs to be done second")
269 t['depends'] = set([dependency1])
272 t['depends'].add(dependency2)
275 self.assertEqual(t['depends'], set([dependency1, dependency2]))
277 def test_add_to_empty_dependency_set(self):
278 # Adds dependency to task with one dependencies
279 t = Task(self.tw, description="test task")
280 dependency = Task(self.tw, description="needs to be done first")
284 t['depends'].add(dependency)
287 self.assertEqual(t['depends'], set([dependency]))
289 def test_simple_dependency_set_save_repeatedly(self):
290 # Adds only one dependency to task with no dependencies
291 t = Task(self.tw, description="test task")
292 dependency = Task(self.tw, description="needs to be done first")
295 t['depends'] = set([dependency])
298 # We taint the task, but keep depends intact
299 t['description'] = "test task modified"
302 self.assertEqual(t['depends'], set([dependency]))
304 # We taint the task, but assign the same set to the depends
305 t['depends'] = set([dependency])
306 t['description'] = "test task modified again"
309 self.assertEqual(t['depends'], set([dependency]))
311 def test_compare_different_tasks(self):
312 # Negative: compare two different tasks
313 t1 = Task(self.tw, description="test task")
314 t2 = Task(self.tw, description="test task")
319 self.assertEqual(t1 == t2, False)
321 def test_compare_same_task_object(self):
322 # Compare Task object wit itself
323 t = Task(self.tw, description="test task")
326 self.assertEqual(t == t, True)
328 def test_compare_same_task(self):
329 # Compare the same task using two different objects
330 t1 = Task(self.tw, description="test task")
333 t2 = self.tw.tasks.get(uuid=t1['uuid'])
334 self.assertEqual(t1 == t2, True)
336 def test_compare_unsaved_tasks(self):
337 # t1 and t2 are unsaved tasks, considered to be unequal
338 # despite the content of data
339 t1 = Task(self.tw, description="test task")
340 t2 = Task(self.tw, description="test task")
342 self.assertEqual(t1 == t2, False)
344 def test_hash_unsaved_tasks(self):
345 # Considered equal, it's the same object
346 t1 = Task(self.tw, description="test task")
348 self.assertEqual(hash(t1) == hash(t2), True)
350 def test_hash_same_task(self):
351 # Compare the hash of the task using two different objects
352 t1 = Task(self.tw, description="test task")
355 t2 = self.tw.tasks.get(uuid=t1['uuid'])
356 self.assertEqual(t1.__hash__(), t2.__hash__())
358 def test_adding_task_with_priority(self):
359 t = Task(self.tw, description="test task", priority="M")
362 def test_removing_priority_with_none(self):
363 t = Task(self.tw, description="test task", priority="L")
366 # Remove the priority mark
370 # Assert that priority is not there after saving
371 self.assertEqual(t['priority'], None)
373 def test_adding_task_with_due_time(self):
374 t = Task(self.tw, description="test task", due=datetime.datetime.now())
377 def test_removing_due_time_with_none(self):
378 t = Task(self.tw, description="test task", due=datetime.datetime.now())
381 # Remove the due timestamp
385 # Assert that due timestamp is no longer there
386 self.assertEqual(t['due'], None)
388 def test_modified_fields_new_task(self):
391 # This should be empty with new task
392 self.assertEqual(set(t._modified_fields), set())
395 t['description'] = "test task"
396 self.assertEqual(set(t._modified_fields), set(['description']))
398 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
399 self.assertEqual(set(t._modified_fields), set(['description', 'due']))
401 t['project'] = "test project"
402 self.assertEqual(set(t._modified_fields),
403 set(['description', 'due', 'project']))
405 # List of modified fields should clear out when saved
407 self.assertEqual(set(t._modified_fields), set())
409 # Reassigning the fields with the same values now should not produce
411 t['description'] = "test task"
412 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
413 t['project'] = "test project"
414 self.assertEqual(set(t._modified_fields), set())
416 def test_modified_fields_loaded_task(self):
420 t['description'] = "test task"
421 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
422 t['project'] = "test project"
424 dependency = Task(self.tw, description="dependency")
426 t['depends'] = set([dependency])
428 # List of modified fields should clear out when saved
430 self.assertEqual(set(t._modified_fields), set())
432 # Get the task by using a filter by UUID
433 t2 = self.tw.tasks.get(uuid=t['uuid'])
435 # Reassigning the fields with the same values now should not produce
437 t['description'] = "test task"
438 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
439 t['project'] = "test project"
440 t['depends'] = set([dependency])
441 self.assertEqual(set(t._modified_fields), set())
443 def test_modified_fields_not_affected_by_reading(self):
446 for field in TASK_STANDARD_ATTRS:
449 self.assertEqual(set(t._modified_fields), set())
451 def test_setting_read_only_attrs_through_init(self):
452 # Test that we are unable to set readonly attrs through __init__
453 for readonly_key in Task.read_only_fields:
454 kwargs = {'description': 'test task', readonly_key: 'value'}
455 self.assertRaises(RuntimeError,
456 lambda: Task(self.tw, **kwargs))
458 def test_setting_read_only_attrs_through_setitem(self):
459 # Test that we are unable to set readonly attrs through __init__
460 for readonly_key in Task.read_only_fields:
461 t = Task(self.tw, description='test task')
462 self.assertRaises(RuntimeError,
463 lambda: t.__setitem__(readonly_key, 'value'))
465 def test_saving_unmodified_task(self):
466 t = Task(self.tw, description="test task")
470 def test_adding_tag_by_appending(self):
471 t = Task(self.tw, description="test task", tags=['test1'])
473 t['tags'].append('test2')
475 self.assertEqual(t['tags'], ['test1', 'test2'])
477 def test_adding_tag_by_appending_empty(self):
478 t = Task(self.tw, description="test task")
480 t['tags'].append('test')
482 self.assertEqual(t['tags'], ['test'])
484 def test_serializers_returning_empty_string_for_none(self):
485 # Test that any serializer returns '' when passed None
487 serializers = [getattr(t, serializer_name) for serializer_name in
488 filter(lambda x: x.startswith('serialize_'), dir(t))]
489 for serializer in serializers:
490 self.assertEqual(serializer(None), '')
492 def test_deserializer_returning_empty_value_for_empty_string(self):
493 # Test that any deserializer returns empty value when passed ''
495 deserializers = [getattr(t, deserializer_name) for deserializer_name in
496 filter(lambda x: x.startswith('deserialize_'), dir(t))]
497 for deserializer in deserializers:
498 self.assertTrue(deserializer('') in (None, [], set()))
500 def test_normalizers_handling_none(self):
501 # Test that any normalizer can handle None as a valid value
504 # These normalizers are not supposed to handle None
505 exempt_normalizers = ('normalize_uuid', )
507 normalizers = [getattr(t, normalizer_name) for normalizer_name in
508 filter(lambda x: x.startswith('normalize_'), dir(t))
509 if normalizer_name not in exempt_normalizers]
511 for normalizer in normalizers:
514 def test_recurrent_task_generation(self):
515 today = datetime.date.today()
516 t = Task(self.tw, description="brush teeth",
517 due=today, recur="daily")
519 self.assertEqual(len(self.tw.tasks.pending()), 2)
521 class TaskFromHookTest(TasklibTest):
523 input_add_data = six.StringIO(
524 '{"description":"Buy some milk",'
525 '"entry":"20141118T050231Z",'
526 '"status":"pending",'
527 '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
529 input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
530 '{"description":"Buy some milk finally",'
531 '"entry":"20141118T050231Z",'
532 '"status":"completed",'
533 '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
535 exported_raw_data = (
537 '"due":"20150101T232323Z",'
538 '"description":"test task"}')
540 def test_setting_up_from_add_hook_input(self):
541 t = Task.from_input(input_file=self.input_add_data)
542 self.assertEqual(t['description'], "Buy some milk")
543 self.assertEqual(t.pending, True)
545 def test_setting_up_from_modified_hook_input(self):
546 t = Task.from_input(input_file=self.input_modify_data, modify=True)
547 self.assertEqual(t['description'], "Buy some milk finally")
548 self.assertEqual(t.pending, False)
549 self.assertEqual(t.completed, True)
551 self.assertEqual(t._original_data['status'], "pending")
552 self.assertEqual(t._original_data['description'], "Buy some milk")
553 self.assertEqual(set(t._modified_fields),
554 set(['status', 'description']))
556 def test_export_data(self):
557 t = Task(self.tw, description="test task",
559 due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
561 # Check that the output is a permutation of:
562 # {"project":"Home","description":"test task","due":"20150101232323Z"}
563 allowed_segments = self.exported_raw_data[1:-1].split(',')
565 '{' + ','.join(segments) + '}'
566 for segments in itertools.permutations(allowed_segments)
569 self.assertTrue(any(t.export_data() == expected
570 for expected in allowed_output))
572 class TimezoneAwareDatetimeTest(TasklibTest):
575 super(TimezoneAwareDatetimeTest, self).setUp()
576 self.zone = local_zone
577 self.localdate_naive = datetime.datetime(2015,2,2)
578 self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
579 self.localtime_aware = self.zone.localize(self.localtime_naive)
580 self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
582 def test_timezone_naive_datetime_setitem(self):
583 t = Task(self.tw, description="test task")
584 t['due'] = self.localtime_naive
585 self.assertEqual(t['due'], self.localtime_aware)
587 def test_timezone_naive_datetime_using_init(self):
588 t = Task(self.tw, description="test task", due=self.localtime_naive)
589 self.assertEqual(t['due'], self.localtime_aware)
591 def test_filter_by_naive_datetime(self):
592 t = Task(self.tw, description="task1", due=self.localtime_naive)
594 matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
595 self.assertEqual(len(matching_tasks), 1)
597 def test_serialize_naive_datetime(self):
598 t = Task(self.tw, description="task1", due=self.localtime_naive)
599 self.assertEqual(json.loads(t.export_data())['due'],
600 self.utctime_aware.strftime(DATE_FORMAT))
602 def test_timezone_naive_date_setitem(self):
603 t = Task(self.tw, description="test task")
604 t['due'] = self.localdate_naive
605 self.assertEqual(t['due'], self.localtime_aware)
607 def test_timezone_naive_date_using_init(self):
608 t = Task(self.tw, description="test task", due=self.localdate_naive)
609 self.assertEqual(t['due'], self.localtime_aware)
611 def test_filter_by_naive_date(self):
612 t = Task(self.tw, description="task1", due=self.localdate_naive)
614 matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
615 self.assertEqual(len(matching_tasks), 1)
617 def test_serialize_naive_date(self):
618 t = Task(self.tw, description="task1", due=self.localdate_naive)
619 self.assertEqual(json.loads(t.export_data())['due'],
620 self.utctime_aware.strftime(DATE_FORMAT))
622 def test_timezone_aware_datetime_setitem(self):
623 t = Task(self.tw, description="test task")
624 t['due'] = self.localtime_aware
625 self.assertEqual(t['due'], self.localtime_aware)
627 def test_timezone_aware_datetime_using_init(self):
628 t = Task(self.tw, description="test task", due=self.localtime_aware)
629 self.assertEqual(t['due'], self.localtime_aware)
631 def test_filter_by_aware_datetime(self):
632 t = Task(self.tw, description="task1", due=self.localtime_aware)
634 matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
635 self.assertEqual(len(matching_tasks), 1)
637 def test_serialize_aware_datetime(self):
638 t = Task(self.tw, description="task1", due=self.localtime_aware)
639 self.assertEqual(json.loads(t.export_data())['due'],
640 self.utctime_aware.strftime(DATE_FORMAT))
642 class AnnotationTest(TasklibTest):
645 super(AnnotationTest, self).setUp()
646 Task(self.tw, description="test task").save()
648 def test_adding_annotation(self):
649 task = self.tw.tasks.get()
650 task.add_annotation('test annotation')
651 self.assertEqual(len(task['annotations']), 1)
652 ann = task['annotations'][0]
653 self.assertEqual(ann['description'], 'test annotation')
655 def test_removing_annotation(self):
656 task = self.tw.tasks.get()
657 task.add_annotation('test annotation')
658 ann = task['annotations'][0]
660 self.assertEqual(len(task['annotations']), 0)
662 def test_removing_annotation_by_description(self):
663 task = self.tw.tasks.get()
664 task.add_annotation('test annotation')
665 task.remove_annotation('test annotation')
666 self.assertEqual(len(task['annotations']), 0)
668 def test_removing_annotation_by_obj(self):
669 task = self.tw.tasks.get()
670 task.add_annotation('test annotation')
671 ann = task['annotations'][0]
672 task.remove_annotation(ann)
673 self.assertEqual(len(task['annotations']), 0)
675 def test_annotation_after_modification(self):
676 task = self.tw.tasks.get()
677 task['project'] = 'test'
678 task.add_annotation('I should really do this task')
679 self.assertEqual(task['project'], 'test')
681 self.assertEqual(task['project'], 'test')
683 def test_serialize_annotations(self):
684 # Test that serializing annotations is possible
685 t = Task(self.tw, description="test")
688 t.add_annotation("annotation1")
689 t.add_annotation("annotation2")
691 data = t._serialize('annotations', t._data['annotations'])
693 self.assertEqual(len(data), 2)
694 self.assertEqual(type(data[0]), dict)
695 self.assertEqual(type(data[1]), dict)
697 self.assertEqual(data[0]['description'], "annotation1")
698 self.assertEqual(data[1]['description'], "annotation2")
701 class UnicodeTest(TasklibTest):
703 def test_unicode_task(self):
704 Task(self.tw, description="†åßk").save()
707 def test_non_unicode_task(self):
708 Task(self.tw, description="test task").save()