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.
15 from .backends import TaskWarrior
16 from .task import Task, ReadOnlyDictView
17 from .lazy import LazyUUIDTask, LazyUUIDTaskSet
18 from .serializing import DATE_FORMAT, local_zone
20 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
21 TASK_STANDARD_ATTRS = (
45 def total_seconds_2_6(x):
46 return x.microseconds / 1e6 + x.seconds + x.days * 24 * 3600
49 class TasklibTest(unittest.TestCase):
51 def get_taskwarrior(self, **kwargs):
53 data_location=self.tmp,
56 tw_kwargs.update(kwargs)
57 return TaskWarrior(**tw_kwargs)
60 self.tmp = tempfile.mkdtemp(dir='.')
61 self.tw = self.get_taskwarrior()
64 shutil.rmtree(self.tmp)
67 class TaskWarriorTest(TasklibTest):
69 def test_custom_command(self):
70 # ensure that a custom command which contains multiple parts
71 # is properly split up
72 tw = self.get_taskwarrior(
73 task_command='wsl task',
74 # prevent `_get_version` from running as `wsl` may not exist
75 version_override=os.getenv('TASK_VERSION'),
77 self.assertEqual(tw._get_task_command(), ['wsl', 'task'])
80 class TaskFilterTest(TasklibTest):
82 def test_all_empty(self):
83 self.assertEqual(len(self.tw.tasks.all()), 0)
85 def test_all_non_empty(self):
86 Task(self.tw, description='test task').save()
87 self.assertEqual(len(self.tw.tasks.all()), 1)
88 self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
89 self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
91 def test_pending_non_empty(self):
92 Task(self.tw, description='test task').save()
93 self.assertEqual(len(self.tw.tasks.pending()), 1)
95 self.tw.tasks.pending()[0]['description'],
98 self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
100 def test_completed_empty(self):
101 Task(self.tw, description='test task').save()
102 self.assertEqual(len(self.tw.tasks.completed()), 0)
104 def test_completed_non_empty(self):
105 Task(self.tw, description='test task').save()
106 self.assertEqual(len(self.tw.tasks.completed()), 0)
107 self.tw.tasks.all()[0].done()
108 self.assertEqual(len(self.tw.tasks.completed()), 1)
110 def test_deleted_empty(self):
111 Task(self.tw, description='test task').save()
112 self.assertEqual(len(self.tw.tasks.deleted()), 0)
114 def test_deleted_non_empty(self):
115 Task(self.tw, description='test task').save()
116 self.assertEqual(len(self.tw.tasks.deleted()), 0)
117 self.tw.tasks.all()[0].delete()
118 self.assertEqual(len(self.tw.tasks.deleted()), 1)
120 def test_waiting_empty(self):
121 Task(self.tw, description='test task').save()
122 self.assertEqual(len(self.tw.tasks.waiting()), 0)
124 def test_waiting_non_empty(self):
125 Task(self.tw, description='test task').save()
126 self.assertEqual(len(self.tw.tasks.waiting()), 0)
128 t = self.tw.tasks.all()[0]
129 t['wait'] = datetime.datetime.now() + datetime.timedelta(days=1)
132 self.assertEqual(len(self.tw.tasks.waiting()), 1)
134 def test_recurring_empty(self):
135 Task(self.tw, description='test task').save()
136 self.assertEqual(len(self.tw.tasks.recurring()), 0)
138 def test_recurring_non_empty(self):
141 description='test task',
143 due=datetime.datetime.now(),
145 self.assertEqual(len(self.tw.tasks.recurring()), 1)
147 def test_filtering_by_attribute(self):
148 Task(self.tw, description='no priority task').save()
149 Task(self.tw, priority='H', description='high priority task').save()
150 self.assertEqual(len(self.tw.tasks.all()), 2)
152 # Assert that the correct number of tasks is returned
153 self.assertEqual(len(self.tw.tasks.filter(priority='H')), 1)
155 # Assert that the correct tasks are returned
156 high_priority_task = self.tw.tasks.get(priority='H')
158 high_priority_task['description'],
159 'high priority task',
162 def test_filtering_by_empty_attribute(self):
163 Task(self.tw, description='no priority task').save()
164 Task(self.tw, priority='H', description='high priority task').save()
165 self.assertEqual(len(self.tw.tasks.all()), 2)
167 # Assert that the correct number of tasks is returned
168 self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
170 # Assert that the correct tasks are returned
171 no_priority_task = self.tw.tasks.get(priority=None)
172 self.assertEqual(no_priority_task['description'], 'no priority task')
174 def test_filter_for_task_with_space_in_descripition(self):
175 task = Task(self.tw, description='test task')
178 filtered_task = self.tw.tasks.get(description='test task')
179 self.assertEqual(filtered_task['description'], 'test task')
181 def test_filter_for_task_without_space_in_descripition(self):
182 task = Task(self.tw, description='test')
185 filtered_task = self.tw.tasks.get(description='test')
186 self.assertEqual(filtered_task['description'], 'test')
188 def test_filter_for_task_with_space_in_project(self):
189 task = Task(self.tw, description='test', project='random project')
192 filtered_task = self.tw.tasks.get(project='random project')
193 self.assertEqual(filtered_task['project'], 'random project')
195 def test_filter_for_task_without_space_in_project(self):
196 task = Task(self.tw, description='test', project='random')
199 filtered_task = self.tw.tasks.get(project='random')
200 self.assertEqual(filtered_task['project'], 'random')
202 def test_filter_with_empty_uuid(self):
203 self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid=''))
205 def test_filter_dummy_by_status(self):
206 t = Task(self.tw, description='test')
209 tasks = self.tw.tasks.filter(status=t['status'])
210 self.assertEqual(list(tasks), [t])
212 def test_filter_dummy_by_uuid(self):
213 t = Task(self.tw, description='test')
216 tasks = self.tw.tasks.filter(uuid=t['uuid'])
217 self.assertEqual(list(tasks), [t])
219 def test_filter_dummy_by_entry(self):
220 t = Task(self.tw, description='test')
223 tasks = self.tw.tasks.filter(entry=t['entry'])
224 self.assertEqual(list(tasks), [t])
226 def test_filter_dummy_by_description(self):
227 t = Task(self.tw, description='test')
230 tasks = self.tw.tasks.filter(description=t['description'])
231 self.assertEqual(list(tasks), [t])
233 def test_filter_dummy_by_start(self):
234 t = Task(self.tw, description='test')
238 tasks = self.tw.tasks.filter(start=t['start'])
239 self.assertEqual(list(tasks), [t])
241 def test_filter_dummy_by_end(self):
242 t = Task(self.tw, description='test')
246 tasks = self.tw.tasks.filter(end=t['end'])
247 self.assertEqual(list(tasks), [t])
249 def test_filter_dummy_by_due(self):
250 t = Task(self.tw, description='test', due=datetime.datetime.now())
253 tasks = self.tw.tasks.filter(due=t['due'])
254 self.assertEqual(list(tasks), [t])
256 def test_filter_dummy_by_until(self):
257 t = Task(self.tw, description='test')
260 tasks = self.tw.tasks.filter(until=t['until'])
261 self.assertEqual(list(tasks), [t])
263 def test_filter_dummy_by_modified(self):
264 # Older TW version does not support bumping modified
266 if self.tw.version < six.text_type('2.2.0'):
267 # Python2.6 does not support SkipTest. As a workaround
268 # mark the test as passed by exiting.
269 if getattr(unittest, 'SkipTest', None) is not None:
270 raise unittest.SkipTest()
274 t = Task(self.tw, description='test')
277 tasks = self.tw.tasks.filter(modified=t['modified'])
278 self.assertEqual(list(tasks), [t])
280 def test_filter_dummy_by_scheduled(self):
281 t = Task(self.tw, description='test')
284 tasks = self.tw.tasks.filter(scheduled=t['scheduled'])
285 self.assertEqual(list(tasks), [t])
287 def test_filter_dummy_by_tags(self):
288 t = Task(self.tw, description='test', tags=['home'])
291 tasks = self.tw.tasks.filter(tags=t['tags'])
292 self.assertEqual(list(tasks), [t])
294 def test_filter_dummy_by_projects(self):
295 t = Task(self.tw, description='test', project='random')
298 tasks = self.tw.tasks.filter(project=t['project'])
299 self.assertEqual(list(tasks), [t])
301 def test_filter_by_priority(self):
302 t = Task(self.tw, description='test', priority='H')
305 tasks = self.tw.tasks.filter(priority=t['priority'])
306 self.assertEqual(list(tasks), [t])
309 class TaskTest(TasklibTest):
311 def test_create_unsaved_task(self):
312 # Make sure a new task is not saved unless explicitly called for
313 Task(self.tw, description='test task')
314 self.assertEqual(len(self.tw.tasks.all()), 0)
316 # TODO: once python 2.6 compatibility is over, use context managers here
317 # and in all subsequent tests for assertRaises
319 def test_delete_unsaved_task(self):
320 t = Task(self.tw, description='test task')
321 self.assertRaises(Task.NotSaved, t.delete)
323 def test_complete_unsaved_task(self):
324 t = Task(self.tw, description='test task')
325 self.assertRaises(Task.NotSaved, t.done)
327 def test_refresh_unsaved_task(self):
328 t = Task(self.tw, description='test task')
329 self.assertRaises(Task.NotSaved, t.refresh)
331 def test_start_unsaved_task(self):
332 t = Task(self.tw, description='test task')
333 self.assertRaises(Task.NotSaved, t.start)
335 def test_delete_deleted_task(self):
336 t = Task(self.tw, description='test task')
340 self.assertRaises(Task.DeletedTask, t.delete)
342 def test_complete_completed_task(self):
343 t = Task(self.tw, description='test task')
347 self.assertRaises(Task.CompletedTask, t.done)
349 def test_start_completed_task(self):
350 t = Task(self.tw, description='test task')
354 self.assertRaises(Task.CompletedTask, t.start)
356 def test_add_completed_task(self):
361 end=datetime.datetime.now(),
365 def test_add_multiple_completed_tasks(self):
370 end=datetime.datetime.now(),
376 end=datetime.datetime.now(),
381 def test_complete_deleted_task(self):
382 t = Task(self.tw, description='test task')
386 self.assertRaises(Task.DeletedTask, t.done)
388 def test_starting_task(self):
389 t = Task(self.tw, description='test task')
390 now = t.datetime_normalizer(datetime.datetime.now())
394 self.assertTrue(now.replace(microsecond=0) <= t['start'])
395 self.assertEqual(t['status'], 'pending')
397 def test_completing_task(self):
398 t = Task(self.tw, description='test task')
399 now = t.datetime_normalizer(datetime.datetime.now())
403 self.assertTrue(now.replace(microsecond=0) <= t['end'])
404 self.assertEqual(t['status'], 'completed')
406 def test_deleting_task(self):
407 t = Task(self.tw, description='test task')
408 now = t.datetime_normalizer(datetime.datetime.now())
412 self.assertTrue(now.replace(microsecond=0) <= t['end'])
413 self.assertEqual(t['status'], 'deleted')
415 def test_started_task_active(self):
416 t = Task(self.tw, description='test task')
419 self.assertTrue(t.active)
421 def test_unstarted_task_inactive(self):
422 t = Task(self.tw, description='test task')
423 self.assertFalse(t.active)
425 self.assertFalse(t.active)
427 def test_start_active_task(self):
428 t = Task(self.tw, description='test task')
431 self.assertRaises(Task.ActiveTask, t.start)
433 def test_stop_completed_task(self):
434 t = Task(self.tw, description='test task')
439 self.assertRaises(Task.InactiveTask, t.stop)
441 t = Task(self.tw, description='test task')
445 self.assertRaises(Task.InactiveTask, t.stop)
447 def test_stop_deleted_task(self):
448 t = Task(self.tw, description='test task')
454 def test_stop_inactive_task(self):
455 t = Task(self.tw, description='test task')
458 self.assertRaises(Task.InactiveTask, t.stop)
460 t = Task(self.tw, description='test task')
465 self.assertRaises(Task.InactiveTask, t.stop)
467 def test_stopping_task(self):
468 t = Task(self.tw, description='test task')
469 t.datetime_normalizer(datetime.datetime.now())
474 self.assertEqual(t['end'], None)
475 self.assertEqual(t['status'], 'pending')
476 self.assertFalse(t.active)
478 def test_modify_simple_attribute_without_space(self):
479 t = Task(self.tw, description='test')
482 self.assertEqual(t['description'], 'test')
484 t['description'] = 'test-modified'
487 self.assertEqual(t['description'], 'test-modified')
489 def test_modify_simple_attribute_with_space(self):
490 # Space can pose problems with parsing
491 t = Task(self.tw, description='test task')
494 self.assertEqual(t['description'], 'test task')
496 t['description'] = 'test task modified'
499 self.assertEqual(t['description'], 'test task modified')
501 def test_empty_dependency_set_of_unsaved_task(self):
502 t = Task(self.tw, description='test task')
503 self.assertEqual(t['depends'], set())
505 def test_empty_dependency_set_of_saved_task(self):
506 t = Task(self.tw, description='test task')
508 self.assertEqual(t['depends'], set())
510 def test_set_unsaved_task_as_dependency(self):
511 # Adds only one dependency to task with no dependencies
512 t = Task(self.tw, description='test task')
513 dependency = Task(self.tw, description='needs to be done first')
515 # We only save the parent task, dependency task is unsaved
517 t['depends'] = set([dependency])
519 self.assertRaises(Task.NotSaved, t.save)
521 def test_set_simple_dependency_set(self):
522 # Adds only one dependency to task with no dependencies
523 t = Task(self.tw, description='test task')
524 dependency = Task(self.tw, description='needs to be done first')
529 t['depends'] = set([dependency])
531 self.assertEqual(t['depends'], set([dependency]))
533 def test_set_simple_dependency_lazyuuidtaskset(self):
534 # Adds only one dependency as a LazyUUIDTaskSet to task with no dependencies
535 t = Task(self.tw, description='test task')
536 dependency = Task(self.tw, description='needs to be done first')
541 t['depends'] = LazyUUIDTaskSet(self.tw, [dependency['uuid']])
543 self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency['uuid']]))
545 def test_set_complex_dependency_set(self):
546 # Adds two dependencies to task with no dependencies
547 t = Task(self.tw, description='test task')
548 dependency1 = Task(self.tw, description='needs to be done first')
549 dependency2 = Task(self.tw, description='needs to be done second')
555 t['depends'] = set([dependency1, dependency2])
557 self.assertEqual(t['depends'], set([dependency1, dependency2]))
559 def test_set_complex_dependency_lazyuuidtaskset(self):
560 # Adds two dependencies as a LazyUUIDTaskSet to task with no dependencies
561 t = Task(self.tw, description='test task')
562 dependency1 = Task(self.tw, description='needs to be done first')
563 dependency2 = Task(self.tw, description='needs to be done second')
569 t['depends'] = LazyUUIDTaskSet(self.tw, [dependency1['uuid'], dependency2['uuid']])
571 self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency1['uuid'], dependency2['uuid']]))
573 def test_remove_from_dependency_set(self):
574 # Removes dependency from task with two dependencies
575 t = Task(self.tw, description='test task')
576 dependency1 = Task(self.tw, description='needs to be done first')
577 dependency2 = Task(self.tw, description='needs to be done second')
582 t['depends'] = set([dependency1, dependency2])
585 t['depends'].remove(dependency2)
588 self.assertEqual(t['depends'], set([dependency1]))
590 def test_remove_from_dependency_lazyuuidtaskset(self):
591 # Removes dependency from task with two dependencies as LazyUUIDTaskSet
592 t = Task(self.tw, description='test task')
593 dependency1 = Task(self.tw, description='needs to be done first')
594 dependency2 = Task(self.tw, description='needs to be done second')
599 t['depends'] = LazyUUIDTaskSet(self.tw, [dependency1['uuid'], dependency2['uuid']])
602 t['depends'].remove(dependency2)
605 self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency1['uuid']]))
607 def test_add_to_dependency_set(self):
608 # Adds dependency to task with one dependencies
609 t = Task(self.tw, description='test task')
610 dependency1 = Task(self.tw, description='needs to be done first')
611 dependency2 = Task(self.tw, description='needs to be done second')
616 t['depends'] = set([dependency1])
619 t['depends'].add(dependency2)
622 self.assertEqual(t['depends'], set([dependency1, dependency2]))
624 def test_add_to_dependency_lazyuuidtaskset(self):
625 # Adds dependency to task with one dependencies as LazyUUIDTaskSet
626 t = Task(self.tw, description='test task')
627 dependency1 = Task(self.tw, description='needs to be done first')
628 dependency2 = Task(self.tw, description='needs to be done second')
633 t['depends'] = LazyUUIDTaskSet(self.tw, [dependency1['uuid']])
636 t['depends'].add(dependency2)
639 self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency1['uuid'], dependency2['uuid']]))
641 def test_add_lazyuuidtaskset_to_dependency_lazyuuidtaskset(self):
642 # Adds dependency as LazyUUIDTaskSet to task with one dependencies as LazyUUIDTaskSet
643 t = Task(self.tw, description='test task')
644 dependency1 = Task(self.tw, description='needs to be done first')
645 dependency2 = Task(self.tw, description='needs to be done second')
650 t['depends'] = LazyUUIDTaskSet(self.tw, [dependency1['uuid']])
653 t['depends'] = LazyUUIDTaskSet(self.tw, [dependency2['uuid']]).union(t['depends'])
656 self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency1['uuid'], dependency2['uuid']]))
658 def test_add_to_empty_dependency_set(self):
659 # Adds dependency to task with no dependencies
660 t = Task(self.tw, description='test task')
661 dependency = Task(self.tw, description='needs to be done first')
665 t['depends'].add(dependency)
668 self.assertEqual(t['depends'], set([dependency]))
670 def test_add_to_empty_dependency_lazyuuidtaskset(self):
671 # Adds dependency as LazyUUIDTaskSet to task with no dependencies
672 t = Task(self.tw, description='test task')
673 dependency = Task(self.tw, description='needs to be done first')
677 t['depends'] = LazyUUIDTaskSet(self.tw, [dependency['uuid']])
680 self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency['uuid']]))
682 def test_simple_dependency_set_save_repeatedly(self):
683 # Adds only one dependency to task with no dependencies
684 t = Task(self.tw, description='test task')
685 dependency = Task(self.tw, description='needs to be done first')
688 t['depends'] = set([dependency])
691 # We taint the task, but keep depends intact
692 t['description'] = 'test task modified'
695 self.assertEqual(t['depends'], set([dependency]))
697 # We taint the task, but assign the same set to the depends
698 t['depends'] = set([dependency])
699 t['description'] = 'test task modified again'
702 self.assertEqual(t['depends'], set([dependency]))
704 def test_simple_dependency_lazyuuidtaskset_save_repeatedly(self):
705 # Adds only one dependency as LazyUUIDTaskSet to task with no dependencies
706 t = Task(self.tw, description='test task')
707 dependency = Task(self.tw, description='needs to be done first')
710 t['depends'] = LazyUUIDTaskSet(self.tw, [dependency['uuid']])
713 # We taint the task, but keep depends intact
714 t['description'] = 'test task modified'
717 self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency['uuid']]))
719 # We taint the task, but assign the same set to the depends
720 t['depends'] = LazyUUIDTaskSet(self.tw, [dependency['uuid']])
721 t['description'] = 'test task modified again'
724 self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency['uuid']]))
726 def test_simple_dependency_lazyuuidtaskset_save_before_repeatedly(self):
727 # Adds only one dependency as LazyUUIDTaskSet to a saved task with no dependencies
728 t = Task(self.tw, description='test task')
729 dependency = Task(self.tw, description='needs to be done first')
733 t['depends'] = LazyUUIDTaskSet(self.tw, [dependency['uuid']])
736 self.assertEqual(t['depends'], LazyUUIDTaskSet(self.tw, [dependency['uuid']]))
738 def test_compare_different_tasks(self):
739 # Negative: compare two different tasks
740 t1 = Task(self.tw, description='test task')
741 t2 = Task(self.tw, description='test task')
746 self.assertEqual(t1 == t2, False)
748 def test_compare_same_task_object(self):
749 # Compare Task object wit itself
750 t = Task(self.tw, description='test task')
753 self.assertEqual(t == t, True)
755 def test_compare_same_task(self):
756 # Compare the same task using two different objects
757 t1 = Task(self.tw, description='test task')
760 t2 = self.tw.tasks.get(uuid=t1['uuid'])
761 self.assertEqual(t1 == t2, True)
763 def test_compare_unsaved_tasks(self):
764 # t1 and t2 are unsaved tasks, considered to be unequal
765 # despite the content of data
766 t1 = Task(self.tw, description='test task')
767 t2 = Task(self.tw, description='test task')
769 self.assertEqual(t1 == t2, False)
771 def test_hash_unsaved_tasks(self):
772 # Considered equal, it's the same object
773 t1 = Task(self.tw, description='test task')
775 self.assertEqual(hash(t1) == hash(t2), True)
777 def test_hash_same_task(self):
778 # Compare the hash of the task using two different objects
779 t1 = Task(self.tw, description='test task')
782 t2 = self.tw.tasks.get(uuid=t1['uuid'])
783 self.assertEqual(t1.__hash__(), t2.__hash__())
785 def test_hash_unequal_unsaved_tasks(self):
786 # Compare the hash of the task using two different objects
787 t1 = Task(self.tw, description='test task 1')
788 t2 = Task(self.tw, description='test task 2')
790 self.assertNotEqual(t1.__hash__(), t2.__hash__())
792 def test_hash_unequal_saved_tasks(self):
793 # Compare the hash of the task using two different objects
794 t1 = Task(self.tw, description='test task 1')
795 t2 = Task(self.tw, description='test task 2')
800 self.assertNotEqual(t1.__hash__(), t2.__hash__())
802 def test_adding_task_with_priority(self):
803 t = Task(self.tw, description='test task', priority='M')
806 def test_removing_priority_with_none(self):
807 t = Task(self.tw, description='test task', priority='L')
810 # Remove the priority mark
814 # Assert that priority is not there after saving
815 self.assertEqual(t['priority'], None)
817 def test_adding_task_with_due_time(self):
818 t = Task(self.tw, description='test task', due=datetime.datetime.now())
821 def test_removing_due_time_with_none(self):
822 t = Task(self.tw, description='test task', due=datetime.datetime.now())
825 # Remove the due timestamp
829 # Assert that due timestamp is no longer there
830 self.assertEqual(t['due'], None)
832 def test_modified_fields_new_task(self):
835 # This should be empty with new task
836 self.assertEqual(set(t._modified_fields), set())
839 t['description'] = 'test task'
840 self.assertEqual(set(t._modified_fields), set(['description']))
842 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
843 self.assertEqual(set(t._modified_fields), set(['description', 'due']))
845 t['project'] = 'test project'
847 set(t._modified_fields),
848 set(['description', 'due', 'project']),
851 # List of modified fields should clear out when saved
853 self.assertEqual(set(t._modified_fields), set())
855 # Reassigning the fields with the same values now should not produce
857 t['description'] = 'test task'
858 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
859 t['project'] = 'test project'
860 self.assertEqual(set(t._modified_fields), set())
862 def test_modified_fields_loaded_task(self):
866 t['description'] = 'test task'
867 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
868 t['project'] = 'test project'
870 dependency = Task(self.tw, description='dependency')
872 t['depends'] = set([dependency])
874 # List of modified fields should clear out when saved
876 self.assertEqual(set(t._modified_fields), set())
878 # Get the task by using a filter by UUID
879 self.tw.tasks.get(uuid=t['uuid'])
881 # Reassigning the fields with the same values now should not produce
883 t['description'] = 'test task'
884 t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14) # <3
885 t['project'] = 'test project'
886 t['depends'] = set([dependency])
887 self.assertEqual(set(t._modified_fields), set())
889 def test_modified_fields_not_affected_by_reading(self):
892 for field in TASK_STANDARD_ATTRS:
895 self.assertEqual(set(t._modified_fields), set())
897 def test_setting_read_only_attrs_through_init(self):
898 # Test that we are unable to set readonly attrs through __init__
899 for readonly_key in Task.read_only_fields:
900 kwargs = {'description': 'test task', readonly_key: 'value'}
903 lambda: Task(self.tw, **kwargs),
906 def test_setting_read_only_attrs_through_setitem(self):
907 # Test that we are unable to set readonly attrs through __init__
908 for readonly_key in Task.read_only_fields:
909 t = Task(self.tw, description='test task')
912 lambda: t.__setitem__(readonly_key, 'value'),
915 def test_saving_unmodified_task(self):
916 t = Task(self.tw, description='test task')
920 def test_adding_tag_by_appending(self):
921 t = Task(self.tw, description='test task', tags=['test1'])
923 t['tags'].add('test2')
925 self.assertEqual(t['tags'], set(['test1', 'test2']))
927 def test_adding_tag_twice(self):
928 t = Task(self.tw, description='test task', tags=['test1'])
930 t['tags'].add('test2')
931 t['tags'].add('test2')
933 self.assertEqual(t['tags'], set(['test1', 'test2']))
935 def test_adding_tag_by_appending_empty(self):
936 t = Task(self.tw, description='test task')
938 t['tags'].add('test')
940 self.assertEqual(t['tags'], set(['test']))
942 def test_serializers_returning_empty_string_for_none(self):
943 # Test that any serializer returns '' when passed None
946 getattr(t, serializer_name)
947 for serializer_name in filter(
948 lambda x: x.startswith('serialize_'),
952 for serializer in serializers:
953 self.assertEqual(serializer(None), '')
955 def test_deserializer_returning_empty_value_for_empty_string(self):
956 # Test that any deserializer returns empty value when passed ''
959 getattr(t, deserializer_name)
960 for deserializer_name in filter(
961 lambda x: x.startswith('deserialize_'),
965 for deserializer in deserializers:
966 self.assertTrue(deserializer('') in (None, [], set()))
968 def test_normalizers_handling_none(self):
969 # Test that any normalizer can handle None as a valid value
972 for key in TASK_STANDARD_ATTRS:
973 t._normalize(key, None)
975 def test_recurrent_task_generation(self):
976 today = datetime.date.today()
979 description='brush teeth',
984 self.assertEqual(len(self.tw.tasks.pending()), 2)
986 def test_spawned_task_parent(self):
987 today = datetime.date.today()
990 description='brush teeth',
996 spawned = self.tw.tasks.pending().get(due=today)
997 assert spawned['parent'] == t
999 def test_modify_number_of_tasks_at_once(self):
1000 for i in range(1, 100):
1001 Task(self.tw, description='test task %d' % i, tags=['test']).save()
1003 self.tw.execute_command(['+test', 'mod', 'unified', 'description'])
1005 def test_return_all_from_executed_command(self):
1006 Task(self.tw, description='test task', tags=['test']).save()
1007 out, err, rc = self.tw.execute_command(['count'], return_all=True)
1008 self.assertEqual(rc, 0)
1010 def test_return_all_from_failed_executed_command(self):
1011 Task(self.tw, description='test task', tags=['test']).save()
1012 out, err, rc = self.tw.execute_command(
1015 allow_failure=False,
1017 self.assertNotEqual(rc, 0)
1020 class TaskFromHookTest(TasklibTest):
1022 input_add_data = six.StringIO(
1023 '{"description":"Buy some milk",'
1024 '"entry":"20141118T050231Z",'
1025 '"status":"pending",'
1026 '"start":"20141119T152233Z",'
1027 '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}',
1030 input_add_data_recurring = six.StringIO(
1031 '{"description":"Mow the lawn",'
1032 '"entry":"20160210T224304Z",'
1033 '"parent":"62da6227-519c-42c2-915d-dccada926ad7",'
1035 '"status":"pending",'
1036 '"uuid":"81305335-0237-49ff-8e87-b3cdc2369cec"}',
1039 input_modify_data = six.StringIO(
1041 input_add_data.getvalue(),
1043 '{"description":"Buy some milk finally",'
1044 '"entry":"20141118T050231Z",'
1045 '"status":"completed",'
1046 '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}'
1051 exported_raw_data = (
1052 '{"project":"Home",'
1053 '"due":"20150101T232323Z",'
1054 '"description":"test task"}'
1057 def test_setting_up_from_add_hook_input(self):
1058 t = Task.from_input(input_file=self.input_add_data, backend=self.tw)
1059 self.assertEqual(t['description'], 'Buy some milk')
1060 self.assertEqual(t.pending, True)
1062 def test_setting_up_from_add_hook_input_recurring(self):
1063 t = Task.from_input(
1064 input_file=self.input_add_data_recurring,
1067 self.assertEqual(t['description'], 'Mow the lawn')
1068 self.assertEqual(t.pending, True)
1070 def test_setting_up_from_modified_hook_input(self):
1071 t = Task.from_input(
1072 input_file=self.input_modify_data,
1076 self.assertEqual(t['description'], 'Buy some milk finally')
1077 self.assertEqual(t.pending, False)
1078 self.assertEqual(t.completed, True)
1080 self.assertEqual(t._original_data['status'], 'pending')
1081 self.assertEqual(t._original_data['description'], 'Buy some milk')
1083 set(t._modified_fields),
1084 set(['status', 'description', 'start']),
1087 def test_export_data(self):
1090 description='test task',
1092 due=pytz.utc.localize(
1093 datetime.datetime(2015, 1, 1, 23, 23, 23)),
1096 # Check that the output is a permutation of:
1097 # {"project":"Home","description":"test task","due":"20150101232323Z"}
1098 allowed_segments = self.exported_raw_data[1:-1].split(',')
1100 '{' + ','.join(segments) + '}'
1101 for segments in itertools.permutations(allowed_segments)
1105 any(t.export_data() == expected
1106 for expected in allowed_output),
1110 class TimezoneAwareDatetimeTest(TasklibTest):
1113 super(TimezoneAwareDatetimeTest, self).setUp()
1114 self.zone = local_zone
1115 self.localdate_naive = datetime.datetime(2015, 2, 2)
1116 self.localtime_naive = datetime.datetime(2015, 2, 2, 0, 0, 0)
1117 self.localtime_aware = self.zone.localize(self.localtime_naive)
1118 self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
1120 def test_timezone_naive_datetime_setitem(self):
1121 t = Task(self.tw, description='test task')
1122 t['due'] = self.localtime_naive
1123 self.assertEqual(t['due'], self.localtime_aware)
1125 def test_timezone_naive_datetime_using_init(self):
1126 t = Task(self.tw, description='test task', due=self.localtime_naive)
1127 self.assertEqual(t['due'], self.localtime_aware)
1129 def test_filter_by_naive_datetime(self):
1130 t = Task(self.tw, description='task1', due=self.localtime_naive)
1132 matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
1133 self.assertEqual(len(matching_tasks), 1)
1135 def test_serialize_naive_datetime(self):
1136 t = Task(self.tw, description='task1', due=self.localtime_naive)
1138 json.loads(t.export_data())['due'],
1139 self.utctime_aware.strftime(DATE_FORMAT),
1142 def test_timezone_naive_date_setitem(self):
1143 t = Task(self.tw, description='test task')
1144 t['due'] = self.localdate_naive
1145 self.assertEqual(t['due'], self.localtime_aware)
1147 def test_timezone_naive_date_using_init(self):
1148 t = Task(self.tw, description='test task', due=self.localdate_naive)
1149 self.assertEqual(t['due'], self.localtime_aware)
1151 def test_filter_by_naive_date(self):
1152 t = Task(self.tw, description='task1', due=self.localdate_naive)
1154 matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
1155 self.assertEqual(len(matching_tasks), 1)
1157 def test_serialize_naive_date(self):
1158 t = Task(self.tw, description='task1', due=self.localdate_naive)
1160 json.loads(t.export_data())['due'],
1161 self.utctime_aware.strftime(DATE_FORMAT),
1164 def test_timezone_aware_datetime_setitem(self):
1165 t = Task(self.tw, description='test task')
1166 t['due'] = self.localtime_aware
1167 self.assertEqual(t['due'], self.localtime_aware)
1169 def test_timezone_aware_datetime_using_init(self):
1170 t = Task(self.tw, description='test task', due=self.localtime_aware)
1171 self.assertEqual(t['due'], self.localtime_aware)
1173 def test_filter_by_aware_datetime(self):
1174 t = Task(self.tw, description='task1', due=self.localtime_aware)
1176 matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
1177 self.assertEqual(len(matching_tasks), 1)
1179 def test_serialize_aware_datetime(self):
1180 t = Task(self.tw, description='task1', due=self.localtime_aware)
1182 json.loads(t.export_data())['due'],
1183 self.utctime_aware.strftime(DATE_FORMAT),
1187 class DatetimeStringTest(TasklibTest):
1189 def test_simple_now_conversion(self):
1190 if self.tw.version < six.text_type('2.4.0'):
1191 # Python2.6 does not support SkipTest. As a workaround
1192 # mark the test as passed by exiting.
1193 if getattr(unittest, 'SkipTest', None) is not None:
1194 raise unittest.SkipTest()
1198 t = Task(self.tw, description='test task', due='now')
1199 now = local_zone.localize(datetime.datetime.now())
1201 # Assert that both times are not more than 5 seconds apart
1202 if sys.version_info < (2, 7):
1203 self.assertTrue(total_seconds_2_6(now - t['due']) < 5)
1204 self.assertTrue(total_seconds_2_6(t['due'] - now) < 5)
1206 self.assertTrue((now - t['due']).total_seconds() < 5)
1207 self.assertTrue((t['due'] - now).total_seconds() < 5)
1209 def test_simple_eoy_conversion(self):
1210 if self.tw.version < six.text_type('2.4.0'):
1211 # Python2.6 does not support SkipTest. As a workaround
1212 # mark the test as passed by exiting.
1213 if getattr(unittest, 'SkipTest', None) is not None:
1214 raise unittest.SkipTest()
1218 t = Task(self.tw, description='test task', due='eoy')
1219 now = local_zone.localize(datetime.datetime.now())
1220 eoy = local_zone.localize(datetime.datetime(
1228 self.assertEqual(eoy, t['due'])
1230 def test_complex_eoy_conversion(self):
1231 if self.tw.version < six.text_type('2.4.0'):
1232 # Python2.6 does not support SkipTest. As a workaround
1233 # mark the test as passed by exiting.
1234 if getattr(unittest, 'SkipTest', None) is not None:
1235 raise unittest.SkipTest()
1239 t = Task(self.tw, description='test task', due='eoy - 4 months')
1240 now = local_zone.localize(datetime.datetime.now())
1241 due_date = local_zone.localize(
1250 ) - datetime.timedelta(0, 4 * 30 * 86400)
1251 self.assertEqual(due_date, t['due'])
1253 def test_filtering_with_string_datetime(self):
1254 if self.tw.version < six.text_type('2.4.0'):
1255 # Python2.6 does not support SkipTest. As a workaround
1256 # mark the test as passed by exiting.
1257 if getattr(unittest, 'SkipTest', None) is not None:
1258 raise unittest.SkipTest()
1264 description='test task',
1265 due=datetime.datetime.now() - datetime.timedelta(0, 2),
1268 self.assertEqual(len(self.tw.tasks.filter(due__before='now')), 1)
1271 class AnnotationTest(TasklibTest):
1274 super(AnnotationTest, self).setUp()
1275 Task(self.tw, description='test task').save()
1277 def test_adding_annotation(self):
1278 task = self.tw.tasks.get()
1279 task.add_annotation('test annotation')
1280 self.assertEqual(len(task['annotations']), 1)
1281 ann = task['annotations'][0]
1282 self.assertEqual(ann['description'], 'test annotation')
1284 def test_removing_annotation(self):
1285 task = self.tw.tasks.get()
1286 task.add_annotation('test annotation')
1287 ann = task['annotations'][0]
1289 self.assertEqual(len(task['annotations']), 0)
1291 def test_removing_annotation_by_description(self):
1292 task = self.tw.tasks.get()
1293 task.add_annotation('test annotation')
1294 task.remove_annotation('test annotation')
1295 self.assertEqual(len(task['annotations']), 0)
1297 def test_removing_annotation_by_obj(self):
1298 task = self.tw.tasks.get()
1299 task.add_annotation('test annotation')
1300 ann = task['annotations'][0]
1301 task.remove_annotation(ann)
1302 self.assertEqual(len(task['annotations']), 0)
1304 def test_annotation_after_modification(self):
1305 task = self.tw.tasks.get()
1306 task['project'] = 'test'
1307 task.add_annotation('I should really do this task')
1308 self.assertEqual(task['project'], 'test')
1310 self.assertEqual(task['project'], 'test')
1312 def test_serialize_annotations(self):
1313 # Test that serializing annotations is possible
1314 t = Task(self.tw, description='test')
1317 t.add_annotation('annotation1')
1318 t.add_annotation('annotation2')
1320 data = t._serialize('annotations', t._data['annotations'])
1322 self.assertEqual(len(data), 2)
1323 self.assertEqual(type(data[0]), dict)
1324 self.assertEqual(type(data[1]), dict)
1326 self.assertEqual(data[0]['description'], 'annotation1')
1327 self.assertEqual(data[1]['description'], 'annotation2')
1330 class UnicodeTest(TasklibTest):
1332 def test_unicode_task(self):
1333 Task(self.tw, description=six.u('†åßk')).save()
1336 def test_filter_by_unicode_task(self):
1337 Task(self.tw, description=six.u('†åßk')).save()
1338 tasks = self.tw.tasks.filter(description=six.u('†åßk'))
1339 self.assertEqual(len(tasks), 1)
1341 def test_non_unicode_task(self):
1342 Task(self.tw, description='test task').save()
1346 class ReadOnlyDictViewTest(unittest.TestCase):
1349 self.sample = dict(sample_list=[1, 2, 3], sample_dict={'key': 'value'})
1350 self.original_sample = copy.deepcopy(self.sample)
1351 self.view = ReadOnlyDictView(self.sample)
1353 def test_readonlydictview_getitem(self):
1354 sample_list = self.view['sample_list']
1355 self.assertEqual(sample_list, self.sample['sample_list'])
1357 # Assert that modification changed only copied value
1358 sample_list.append(4)
1359 self.assertNotEqual(sample_list, self.sample['sample_list'])
1361 # Assert that viewed dict is not changed
1362 self.assertEqual(self.sample, self.original_sample)
1364 def test_readonlydictview_contains(self):
1365 self.assertEqual('sample_list' in self.view,
1366 'sample_list' in self.sample)
1367 self.assertEqual('sample_dict' in self.view,
1368 'sample_dict' in self.sample)
1369 self.assertEqual('key' in self.view, 'key' in self.sample)
1371 # Assert that viewed dict is not changed
1372 self.assertEqual(self.sample, self.original_sample)
1374 def test_readonlydictview_iter(self):
1376 list(key for key in self.view),
1377 list(key for key in self.sample),
1380 # Assert the view is correct after modification
1381 self.sample['new'] = 'value'
1383 list(key for key in self.view),
1384 list(key for key in self.sample),
1387 def test_readonlydictview_len(self):
1388 self.assertEqual(len(self.view), len(self.sample))
1390 # Assert the view is correct after modification
1391 self.sample['new'] = 'value'
1392 self.assertEqual(len(self.view), len(self.sample))
1394 def test_readonlydictview_get(self):
1395 sample_list = self.view.get('sample_list')
1396 self.assertEqual(sample_list, self.sample.get('sample_list'))
1398 # Assert that modification changed only copied value
1399 sample_list.append(4)
1400 self.assertNotEqual(sample_list, self.sample.get('sample_list'))
1402 # Assert that viewed dict is not changed
1403 self.assertEqual(self.sample, self.original_sample)
1405 def test_readonlydict_items(self):
1406 view_items = self.view.items()
1407 sample_items = list(self.sample.items())
1408 self.assertEqual(view_items, sample_items)
1410 view_items.append('newkey')
1411 self.assertNotEqual(view_items, sample_items)
1412 self.assertEqual(self.sample, self.original_sample)
1414 def test_readonlydict_values(self):
1415 view_values = self.view.values()
1416 sample_values = list(self.sample.values())
1417 self.assertEqual(view_values, sample_values)
1419 view_list_item = list(filter(lambda x: type(x) is list,
1421 view_list_item.append(4)
1422 self.assertNotEqual(view_values, sample_values)
1423 self.assertEqual(self.sample, self.original_sample)
1426 class LazyUUIDTaskTest(TasklibTest):
1429 super(LazyUUIDTaskTest, self).setUp()
1431 self.stored = Task(self.tw, description='this is test task')
1434 self.lazy = LazyUUIDTask(self.tw, self.stored['uuid'])
1436 def test_uuid_non_conversion(self):
1437 assert self.stored['uuid'] == self.lazy['uuid']
1438 assert type(self.lazy) is LazyUUIDTask
1440 def test_lazy_explicit_conversion(self):
1441 assert type(self.lazy) is LazyUUIDTask
1443 assert type(self.lazy) is Task
1445 def test_conversion_key(self):
1446 assert self.stored['description'] == self.lazy['description']
1447 assert type(self.lazy) is Task
1449 def test_conversion_attribute(self):
1450 assert type(self.lazy) is LazyUUIDTask
1451 assert self.lazy.completed is False
1452 assert type(self.lazy) is Task
1454 def test_normal_to_lazy_equality(self):
1455 assert self.stored == self.lazy
1456 assert not self.stored != self.lazy
1457 assert type(self.lazy) is LazyUUIDTask
1459 def test_lazy_to_lazy_equality(self):
1460 lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
1461 lazy2 = LazyUUIDTask(self.tw, self.stored['uuid'])
1463 assert lazy1 == lazy2
1464 assert not lazy1 != lazy2
1465 assert type(lazy1) is LazyUUIDTask
1466 assert type(lazy2) is LazyUUIDTask
1468 def test_normal_to_lazy_inequality(self):
1469 # Create a different UUID by changing the last letter
1470 wrong_uuid = self.stored['uuid']
1471 wrong_uuid = wrong_uuid[:-1] + ('a' if wrong_uuid[-1] != 'a' else 'b')
1473 wrong_lazy = LazyUUIDTask(self.tw, wrong_uuid)
1475 assert not self.stored == wrong_lazy
1476 assert self.stored != wrong_lazy
1477 assert type(wrong_lazy) is LazyUUIDTask
1479 def test_lazy_to_lazy_inequality(self):
1480 # Create a different UUID by changing the last letter
1481 wrong_uuid = self.stored['uuid']
1482 wrong_uuid = wrong_uuid[:-1] + ('a' if wrong_uuid[-1] != 'a' else 'b')
1484 lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
1485 lazy2 = LazyUUIDTask(self.tw, wrong_uuid)
1487 assert not lazy1 == lazy2
1488 assert lazy1 != lazy2
1489 assert type(lazy1) is LazyUUIDTask
1490 assert type(lazy2) is LazyUUIDTask
1492 def test_lazy_in_queryset(self):
1493 tasks = self.tw.tasks.filter(uuid=self.stored['uuid'])
1495 assert self.lazy in tasks
1496 assert type(self.lazy) is LazyUUIDTask
1498 def test_lazy_saved(self):
1499 assert self.lazy.saved is True
1501 def test_lazy_modified(self):
1502 assert self.lazy.modified is False
1504 def test_lazy_modified_fields(self):
1505 assert self.lazy._modified_fields == set()
1508 class LazyUUIDTaskSetTest(TasklibTest):
1511 super(LazyUUIDTaskSetTest, self).setUp()
1513 self.task1 = Task(self.tw, description='task 1')
1514 self.task2 = Task(self.tw, description='task 2')
1515 self.task3 = Task(self.tw, description='task 3')
1527 self.lazy = LazyUUIDTaskSet(self.tw, self.uuids)
1529 def test_length(self):
1530 assert len(self.lazy) == 3
1531 assert type(self.lazy) is LazyUUIDTaskSet
1533 def test_contains(self):
1534 assert self.task1 in self.lazy
1535 assert self.task2 in self.lazy
1536 assert self.task3 in self.lazy
1537 assert type(self.lazy) is LazyUUIDTaskSet
1539 def test_eq_lazy(self):
1540 new_lazy = LazyUUIDTaskSet(self.tw, self.uuids)
1541 assert self.lazy == new_lazy
1542 assert not self.lazy != new_lazy
1543 assert type(self.lazy) is LazyUUIDTaskSet
1545 def test_eq_real(self):
1546 assert self.lazy == self.tw.tasks.all()
1547 assert self.tw.tasks.all() == self.lazy
1548 assert not self.lazy != self.tw.tasks.all()
1550 assert type(self.lazy) is LazyUUIDTaskSet
1552 def test_union(self):
1553 taskset = set([self.task1])
1554 lazyset = LazyUUIDTaskSet(
1556 (self.task2['uuid'], self.task3['uuid']),
1559 assert taskset | lazyset == self.lazy
1560 assert lazyset | taskset == self.lazy
1561 assert taskset.union(lazyset) == self.lazy
1562 assert lazyset.union(taskset) == self.lazy
1565 assert lazyset == self.lazy
1567 def test_difference(self):
1568 taskset = set([self.task1, self.task2])
1569 lazyset = LazyUUIDTaskSet(
1571 (self.task2['uuid'], self.task3['uuid']),
1574 assert taskset - lazyset == set([self.task1])
1575 assert lazyset - taskset == set([self.task3])
1576 assert taskset.difference(lazyset) == set([self.task1])
1577 assert lazyset.difference(taskset) == set([self.task3])
1580 assert lazyset == set([self.task3])
1582 def test_symmetric_difference(self):
1583 taskset = set([self.task1, self.task2])
1584 lazyset = LazyUUIDTaskSet(
1586 (self.task2['uuid'], self.task3['uuid']),
1589 assert taskset ^ lazyset == set([self.task1, self.task3])
1590 assert lazyset ^ taskset == set([self.task1, self.task3])
1592 taskset.symmetric_difference(lazyset),
1593 set([self.task1, self.task3]),
1596 lazyset.symmetric_difference(taskset),
1597 set([self.task1, self.task3]),
1601 assert lazyset == set([self.task1, self.task3])
1603 def test_intersection(self):
1604 taskset = set([self.task1, self.task2])
1605 lazyset = LazyUUIDTaskSet(
1607 (self.task2['uuid'], self.task3['uuid']),
1610 assert taskset & lazyset == set([self.task2])
1611 assert lazyset & taskset == set([self.task2])
1612 assert taskset.intersection(lazyset) == set([self.task2])
1613 assert lazyset.intersection(taskset) == set([self.task2])
1616 assert lazyset == set([self.task2])
1619 class TaskWarriorBackendTest(TasklibTest):
1621 def test_config(self):
1622 assert self.tw.config['nag'] == 'You have more urgent tasks.'
1623 assert self.tw.config['default.command'] == 'next'
1624 assert self.tw.config['dependency.indicator'] == 'D'