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.
  14 from .backends import TaskWarrior
 
  15 from .task import Task, ReadOnlyDictView
 
  16 from .lazy import LazyUUIDTask, LazyUUIDTaskSet
 
  17 from .serializing import DATE_FORMAT, local_zone
 
  19 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
 
  20 TASK_STANDARD_ATTRS = (
 
  44 def total_seconds_2_6(x):
 
  45     return x.microseconds / 1e6 + x.seconds + x.days * 24 * 3600
 
  48 class TasklibTest(unittest.TestCase):
 
  51         self.tmp = tempfile.mkdtemp(dir='.')
 
  52         self.tw = TaskWarrior(data_location=self.tmp, taskrc_location='/')
 
  55         shutil.rmtree(self.tmp)
 
  58 class TaskFilterTest(TasklibTest):
 
  60     def test_all_empty(self):
 
  61         self.assertEqual(len(self.tw.tasks.all()), 0)
 
  63     def test_all_non_empty(self):
 
  64         Task(self.tw, description='test task').save()
 
  65         self.assertEqual(len(self.tw.tasks.all()), 1)
 
  66         self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
 
  67         self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
 
  69     def test_pending_non_empty(self):
 
  70         Task(self.tw, description='test task').save()
 
  71         self.assertEqual(len(self.tw.tasks.pending()), 1)
 
  73             self.tw.tasks.pending()[0]['description'],
 
  76         self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
 
  78     def test_completed_empty(self):
 
  79         Task(self.tw, description='test task').save()
 
  80         self.assertEqual(len(self.tw.tasks.completed()), 0)
 
  82     def test_completed_non_empty(self):
 
  83         Task(self.tw, description='test task').save()
 
  84         self.assertEqual(len(self.tw.tasks.completed()), 0)
 
  85         self.tw.tasks.all()[0].done()
 
  86         self.assertEqual(len(self.tw.tasks.completed()), 1)
 
  88     def test_deleted_empty(self):
 
  89         Task(self.tw, description='test task').save()
 
  90         self.assertEqual(len(self.tw.tasks.deleted()), 0)
 
  92     def test_deleted_non_empty(self):
 
  93         Task(self.tw, description='test task').save()
 
  94         self.assertEqual(len(self.tw.tasks.deleted()), 0)
 
  95         self.tw.tasks.all()[0].delete()
 
  96         self.assertEqual(len(self.tw.tasks.deleted()), 1)
 
  98     def test_waiting_empty(self):
 
  99         Task(self.tw, description='test task').save()
 
 100         self.assertEqual(len(self.tw.tasks.waiting()), 0)
 
 102     def test_waiting_non_empty(self):
 
 103         Task(self.tw, description='test task').save()
 
 104         self.assertEqual(len(self.tw.tasks.waiting()), 0)
 
 106         t = self.tw.tasks.all()[0]
 
 107         t['wait'] = datetime.datetime.now() + datetime.timedelta(days=1)
 
 110         self.assertEqual(len(self.tw.tasks.waiting()), 1)
 
 112     def test_recurring_empty(self):
 
 113         Task(self.tw, description='test task').save()
 
 114         self.assertEqual(len(self.tw.tasks.recurring()), 0)
 
 116     def test_recurring_non_empty(self):
 
 119             description='test task',
 
 121             due=datetime.datetime.now(),
 
 123         self.assertEqual(len(self.tw.tasks.recurring()), 1)
 
 125     def test_filtering_by_attribute(self):
 
 126         Task(self.tw, description='no priority task').save()
 
 127         Task(self.tw, priority='H', description='high priority task').save()
 
 128         self.assertEqual(len(self.tw.tasks.all()), 2)
 
 130         # Assert that the correct number of tasks is returned
 
 131         self.assertEqual(len(self.tw.tasks.filter(priority='H')), 1)
 
 133         # Assert that the correct tasks are returned
 
 134         high_priority_task = self.tw.tasks.get(priority='H')
 
 136             high_priority_task['description'],
 
 137             'high priority task',
 
 140     def test_filtering_by_empty_attribute(self):
 
 141         Task(self.tw, description='no priority task').save()
 
 142         Task(self.tw, priority='H', description='high priority task').save()
 
 143         self.assertEqual(len(self.tw.tasks.all()), 2)
 
 145         # Assert that the correct number of tasks is returned
 
 146         self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
 
 148         # Assert that the correct tasks are returned
 
 149         no_priority_task = self.tw.tasks.get(priority=None)
 
 150         self.assertEqual(no_priority_task['description'], 'no priority task')
 
 152     def test_filter_for_task_with_space_in_descripition(self):
 
 153         task = Task(self.tw, description='test task')
 
 156         filtered_task = self.tw.tasks.get(description='test task')
 
 157         self.assertEqual(filtered_task['description'], 'test task')
 
 159     def test_filter_for_task_without_space_in_descripition(self):
 
 160         task = Task(self.tw, description='test')
 
 163         filtered_task = self.tw.tasks.get(description='test')
 
 164         self.assertEqual(filtered_task['description'], 'test')
 
 166     def test_filter_for_task_with_space_in_project(self):
 
 167         task = Task(self.tw, description='test', project='random project')
 
 170         filtered_task = self.tw.tasks.get(project='random project')
 
 171         self.assertEqual(filtered_task['project'], 'random project')
 
 173     def test_filter_for_task_without_space_in_project(self):
 
 174         task = Task(self.tw, description='test', project='random')
 
 177         filtered_task = self.tw.tasks.get(project='random')
 
 178         self.assertEqual(filtered_task['project'], 'random')
 
 180     def test_filter_with_empty_uuid(self):
 
 181         self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid=''))
 
 183     def test_filter_dummy_by_status(self):
 
 184         t = Task(self.tw, description='test')
 
 187         tasks = self.tw.tasks.filter(status=t['status'])
 
 188         self.assertEqual(list(tasks), [t])
 
 190     def test_filter_dummy_by_uuid(self):
 
 191         t = Task(self.tw, description='test')
 
 194         tasks = self.tw.tasks.filter(uuid=t['uuid'])
 
 195         self.assertEqual(list(tasks), [t])
 
 197     def test_filter_dummy_by_entry(self):
 
 198         t = Task(self.tw, description='test')
 
 201         tasks = self.tw.tasks.filter(entry=t['entry'])
 
 202         self.assertEqual(list(tasks), [t])
 
 204     def test_filter_dummy_by_description(self):
 
 205         t = Task(self.tw, description='test')
 
 208         tasks = self.tw.tasks.filter(description=t['description'])
 
 209         self.assertEqual(list(tasks), [t])
 
 211     def test_filter_dummy_by_start(self):
 
 212         t = Task(self.tw, description='test')
 
 216         tasks = self.tw.tasks.filter(start=t['start'])
 
 217         self.assertEqual(list(tasks), [t])
 
 219     def test_filter_dummy_by_end(self):
 
 220         t = Task(self.tw, description='test')
 
 224         tasks = self.tw.tasks.filter(end=t['end'])
 
 225         self.assertEqual(list(tasks), [t])
 
 227     def test_filter_dummy_by_due(self):
 
 228         t = Task(self.tw, description='test', due=datetime.datetime.now())
 
 231         tasks = self.tw.tasks.filter(due=t['due'])
 
 232         self.assertEqual(list(tasks), [t])
 
 234     def test_filter_dummy_by_until(self):
 
 235         t = Task(self.tw, description='test')
 
 238         tasks = self.tw.tasks.filter(until=t['until'])
 
 239         self.assertEqual(list(tasks), [t])
 
 241     def test_filter_dummy_by_modified(self):
 
 242         # Older TW version does not support bumping modified
 
 244         if self.tw.version < six.text_type('2.2.0'):
 
 245             # Python2.6 does not support SkipTest. As a workaround
 
 246             # mark the test as passed by exiting.
 
 247             if getattr(unittest, 'SkipTest', None) is not None:
 
 248                 raise unittest.SkipTest()
 
 252         t = Task(self.tw, description='test')
 
 255         tasks = self.tw.tasks.filter(modified=t['modified'])
 
 256         self.assertEqual(list(tasks), [t])
 
 258     def test_filter_dummy_by_scheduled(self):
 
 259         t = Task(self.tw, description='test')
 
 262         tasks = self.tw.tasks.filter(scheduled=t['scheduled'])
 
 263         self.assertEqual(list(tasks), [t])
 
 265     def test_filter_dummy_by_tags(self):
 
 266         t = Task(self.tw, description='test', tags=['home'])
 
 269         tasks = self.tw.tasks.filter(tags=t['tags'])
 
 270         self.assertEqual(list(tasks), [t])
 
 272     def test_filter_dummy_by_projects(self):
 
 273         t = Task(self.tw, description='test', project='random')
 
 276         tasks = self.tw.tasks.filter(project=t['project'])
 
 277         self.assertEqual(list(tasks), [t])
 
 279     def test_filter_by_priority(self):
 
 280         t = Task(self.tw, description='test', priority='H')
 
 283         tasks = self.tw.tasks.filter(priority=t['priority'])
 
 284         self.assertEqual(list(tasks), [t])
 
 287 class TaskTest(TasklibTest):
 
 289     def test_create_unsaved_task(self):
 
 290         # Make sure a new task is not saved unless explicitly called for
 
 291         Task(self.tw, description='test task')
 
 292         self.assertEqual(len(self.tw.tasks.all()), 0)
 
 294     # TODO: once python 2.6 compatibility is over, use context managers here
 
 295     #       and in all subsequent tests for assertRaises
 
 297     def test_delete_unsaved_task(self):
 
 298         t = Task(self.tw, description='test task')
 
 299         self.assertRaises(Task.NotSaved, t.delete)
 
 301     def test_complete_unsaved_task(self):
 
 302         t = Task(self.tw, description='test task')
 
 303         self.assertRaises(Task.NotSaved, t.done)
 
 305     def test_refresh_unsaved_task(self):
 
 306         t = Task(self.tw, description='test task')
 
 307         self.assertRaises(Task.NotSaved, t.refresh)
 
 309     def test_start_unsaved_task(self):
 
 310         t = Task(self.tw, description='test task')
 
 311         self.assertRaises(Task.NotSaved, t.start)
 
 313     def test_delete_deleted_task(self):
 
 314         t = Task(self.tw, description='test task')
 
 318         self.assertRaises(Task.DeletedTask, t.delete)
 
 320     def test_complete_completed_task(self):
 
 321         t = Task(self.tw, description='test task')
 
 325         self.assertRaises(Task.CompletedTask, t.done)
 
 327     def test_start_completed_task(self):
 
 328         t = Task(self.tw, description='test task')
 
 332         self.assertRaises(Task.CompletedTask, t.start)
 
 334     def test_add_completed_task(self):
 
 339             end=datetime.datetime.now(),
 
 343     def test_add_multiple_completed_tasks(self):
 
 348             end=datetime.datetime.now(),
 
 354             end=datetime.datetime.now(),
 
 359     def test_complete_deleted_task(self):
 
 360         t = Task(self.tw, description='test task')
 
 364         self.assertRaises(Task.DeletedTask, t.done)
 
 366     def test_starting_task(self):
 
 367         t = Task(self.tw, description='test task')
 
 368         now = t.datetime_normalizer(datetime.datetime.now())
 
 372         self.assertTrue(now.replace(microsecond=0) <= t['start'])
 
 373         self.assertEqual(t['status'], 'pending')
 
 375     def test_completing_task(self):
 
 376         t = Task(self.tw, description='test task')
 
 377         now = t.datetime_normalizer(datetime.datetime.now())
 
 381         self.assertTrue(now.replace(microsecond=0) <= t['end'])
 
 382         self.assertEqual(t['status'], 'completed')
 
 384     def test_deleting_task(self):
 
 385         t = Task(self.tw, description='test task')
 
 386         now = t.datetime_normalizer(datetime.datetime.now())
 
 390         self.assertTrue(now.replace(microsecond=0) <= t['end'])
 
 391         self.assertEqual(t['status'], 'deleted')
 
 393     def test_started_task_active(self):
 
 394         t = Task(self.tw, description='test task')
 
 397         self.assertTrue(t.active)
 
 399     def test_unstarted_task_inactive(self):
 
 400         t = Task(self.tw, description='test task')
 
 401         self.assertFalse(t.active)
 
 403         self.assertFalse(t.active)
 
 405     def test_start_active_task(self):
 
 406         t = Task(self.tw, description='test task')
 
 409         self.assertRaises(Task.ActiveTask, t.start)
 
 411     def test_stop_completed_task(self):
 
 412         t = Task(self.tw, description='test task')
 
 417         self.assertRaises(Task.InactiveTask, t.stop)
 
 419         t = Task(self.tw, description='test task')
 
 423         self.assertRaises(Task.InactiveTask, t.stop)
 
 425     def test_stop_deleted_task(self):
 
 426         t = Task(self.tw, description='test task')
 
 432     def test_stop_inactive_task(self):
 
 433         t = Task(self.tw, description='test task')
 
 436         self.assertRaises(Task.InactiveTask, t.stop)
 
 438         t = Task(self.tw, description='test task')
 
 443         self.assertRaises(Task.InactiveTask, t.stop)
 
 445     def test_stopping_task(self):
 
 446         t = Task(self.tw, description='test task')
 
 447         t.datetime_normalizer(datetime.datetime.now())
 
 452         self.assertEqual(t['end'], None)
 
 453         self.assertEqual(t['status'], 'pending')
 
 454         self.assertFalse(t.active)
 
 456     def test_modify_simple_attribute_without_space(self):
 
 457         t = Task(self.tw, description='test')
 
 460         self.assertEqual(t['description'], 'test')
 
 462         t['description'] = 'test-modified'
 
 465         self.assertEqual(t['description'], 'test-modified')
 
 467     def test_modify_simple_attribute_with_space(self):
 
 468         # Space can pose problems with parsing
 
 469         t = Task(self.tw, description='test task')
 
 472         self.assertEqual(t['description'], 'test task')
 
 474         t['description'] = 'test task modified'
 
 477         self.assertEqual(t['description'], 'test task modified')
 
 479     def test_empty_dependency_set_of_unsaved_task(self):
 
 480         t = Task(self.tw, description='test task')
 
 481         self.assertEqual(t['depends'], set())
 
 483     def test_empty_dependency_set_of_saved_task(self):
 
 484         t = Task(self.tw, description='test task')
 
 486         self.assertEqual(t['depends'], set())
 
 488     def test_set_unsaved_task_as_dependency(self):
 
 489         # Adds only one dependency to task with no dependencies
 
 490         t = Task(self.tw, description='test task')
 
 491         dependency = Task(self.tw, description='needs to be done first')
 
 493         # We only save the parent task, dependency task is unsaved
 
 495         t['depends'] = set([dependency])
 
 497         self.assertRaises(Task.NotSaved, t.save)
 
 499     def test_set_simple_dependency_set(self):
 
 500         # Adds only one dependency to task with no dependencies
 
 501         t = Task(self.tw, description='test task')
 
 502         dependency = Task(self.tw, description='needs to be done first')
 
 507         t['depends'] = set([dependency])
 
 509         self.assertEqual(t['depends'], set([dependency]))
 
 511     def test_set_complex_dependency_set(self):
 
 512         # Adds two dependencies to task with no dependencies
 
 513         t = Task(self.tw, description='test task')
 
 514         dependency1 = Task(self.tw, description='needs to be done first')
 
 515         dependency2 = Task(self.tw, description='needs to be done second')
 
 521         t['depends'] = set([dependency1, dependency2])
 
 523         self.assertEqual(t['depends'], set([dependency1, dependency2]))
 
 525     def test_remove_from_dependency_set(self):
 
 526         # Removes dependency from task with two dependencies
 
 527         t = Task(self.tw, description='test task')
 
 528         dependency1 = Task(self.tw, description='needs to be done first')
 
 529         dependency2 = Task(self.tw, description='needs to be done second')
 
 534         t['depends'] = set([dependency1, dependency2])
 
 537         t['depends'].remove(dependency2)
 
 540         self.assertEqual(t['depends'], set([dependency1]))
 
 542     def test_add_to_dependency_set(self):
 
 543         # Adds dependency to task with one dependencies
 
 544         t = Task(self.tw, description='test task')
 
 545         dependency1 = Task(self.tw, description='needs to be done first')
 
 546         dependency2 = Task(self.tw, description='needs to be done second')
 
 551         t['depends'] = set([dependency1])
 
 554         t['depends'].add(dependency2)
 
 557         self.assertEqual(t['depends'], set([dependency1, dependency2]))
 
 559     def test_add_to_empty_dependency_set(self):
 
 560         # Adds dependency to task with one dependencies
 
 561         t = Task(self.tw, description='test task')
 
 562         dependency = Task(self.tw, description='needs to be done first')
 
 566         t['depends'].add(dependency)
 
 569         self.assertEqual(t['depends'], set([dependency]))
 
 571     def test_simple_dependency_set_save_repeatedly(self):
 
 572         # Adds only one dependency to task with no dependencies
 
 573         t = Task(self.tw, description='test task')
 
 574         dependency = Task(self.tw, description='needs to be done first')
 
 577         t['depends'] = set([dependency])
 
 580         # We taint the task, but keep depends intact
 
 581         t['description'] = 'test task modified'
 
 584         self.assertEqual(t['depends'], set([dependency]))
 
 586         # We taint the task, but assign the same set to the depends
 
 587         t['depends'] = set([dependency])
 
 588         t['description'] = 'test task modified again'
 
 591         self.assertEqual(t['depends'], set([dependency]))
 
 593     def test_compare_different_tasks(self):
 
 594         # Negative: compare two different tasks
 
 595         t1 = Task(self.tw, description='test task')
 
 596         t2 = Task(self.tw, description='test task')
 
 601         self.assertEqual(t1 == t2, False)
 
 603     def test_compare_same_task_object(self):
 
 604         # Compare Task object wit itself
 
 605         t = Task(self.tw, description='test task')
 
 608         self.assertEqual(t == t, True)
 
 610     def test_compare_same_task(self):
 
 611         # Compare the same task using two different objects
 
 612         t1 = Task(self.tw, description='test task')
 
 615         t2 = self.tw.tasks.get(uuid=t1['uuid'])
 
 616         self.assertEqual(t1 == t2, True)
 
 618     def test_compare_unsaved_tasks(self):
 
 619         # t1 and t2 are unsaved tasks, considered to be unequal
 
 620         # despite the content of data
 
 621         t1 = Task(self.tw, description='test task')
 
 622         t2 = Task(self.tw, description='test task')
 
 624         self.assertEqual(t1 == t2, False)
 
 626     def test_hash_unsaved_tasks(self):
 
 627         # Considered equal, it's the same object
 
 628         t1 = Task(self.tw, description='test task')
 
 630         self.assertEqual(hash(t1) == hash(t2), True)
 
 632     def test_hash_same_task(self):
 
 633         # Compare the hash of the task using two different objects
 
 634         t1 = Task(self.tw, description='test task')
 
 637         t2 = self.tw.tasks.get(uuid=t1['uuid'])
 
 638         self.assertEqual(t1.__hash__(), t2.__hash__())
 
 640     def test_hash_unequal_unsaved_tasks(self):
 
 641         # Compare the hash of the task using two different objects
 
 642         t1 = Task(self.tw, description='test task 1')
 
 643         t2 = Task(self.tw, description='test task 2')
 
 645         self.assertNotEqual(t1.__hash__(), t2.__hash__())
 
 647     def test_hash_unequal_saved_tasks(self):
 
 648         # Compare the hash of the task using two different objects
 
 649         t1 = Task(self.tw, description='test task 1')
 
 650         t2 = Task(self.tw, description='test task 2')
 
 655         self.assertNotEqual(t1.__hash__(), t2.__hash__())
 
 657     def test_adding_task_with_priority(self):
 
 658         t = Task(self.tw, description='test task', priority='M')
 
 661     def test_removing_priority_with_none(self):
 
 662         t = Task(self.tw, description='test task', priority='L')
 
 665         # Remove the priority mark
 
 669         # Assert that priority is not there after saving
 
 670         self.assertEqual(t['priority'], None)
 
 672     def test_adding_task_with_due_time(self):
 
 673         t = Task(self.tw, description='test task', due=datetime.datetime.now())
 
 676     def test_removing_due_time_with_none(self):
 
 677         t = Task(self.tw, description='test task', due=datetime.datetime.now())
 
 680         # Remove the due timestamp
 
 684         # Assert that due timestamp is no longer there
 
 685         self.assertEqual(t['due'], None)
 
 687     def test_modified_fields_new_task(self):
 
 690         # This should be empty with new task
 
 691         self.assertEqual(set(t._modified_fields), set())
 
 694         t['description'] = 'test task'
 
 695         self.assertEqual(set(t._modified_fields), set(['description']))
 
 697         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
 
 698         self.assertEqual(set(t._modified_fields), set(['description', 'due']))
 
 700         t['project'] = 'test project'
 
 702             set(t._modified_fields),
 
 703             set(['description', 'due', 'project']),
 
 706         # List of modified fields should clear out when saved
 
 708         self.assertEqual(set(t._modified_fields), set())
 
 710         # Reassigning the fields with the same values now should not produce
 
 712         t['description'] = 'test task'
 
 713         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
 
 714         t['project'] = 'test project'
 
 715         self.assertEqual(set(t._modified_fields), set())
 
 717     def test_modified_fields_loaded_task(self):
 
 721         t['description'] = 'test task'
 
 722         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
 
 723         t['project'] = 'test project'
 
 725         dependency = Task(self.tw, description='dependency')
 
 727         t['depends'] = set([dependency])
 
 729         # List of modified fields should clear out when saved
 
 731         self.assertEqual(set(t._modified_fields), set())
 
 733         # Get the task by using a filter by UUID
 
 734         self.tw.tasks.get(uuid=t['uuid'])
 
 736         # Reassigning the fields with the same values now should not produce
 
 738         t['description'] = 'test task'
 
 739         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
 
 740         t['project'] = 'test project'
 
 741         t['depends'] = set([dependency])
 
 742         self.assertEqual(set(t._modified_fields), set())
 
 744     def test_modified_fields_not_affected_by_reading(self):
 
 747         for field in TASK_STANDARD_ATTRS:
 
 750         self.assertEqual(set(t._modified_fields), set())
 
 752     def test_setting_read_only_attrs_through_init(self):
 
 753         # Test that we are unable to set readonly attrs through __init__
 
 754         for readonly_key in Task.read_only_fields:
 
 755             kwargs = {'description': 'test task', readonly_key: 'value'}
 
 758                 lambda: Task(self.tw, **kwargs),
 
 761     def test_setting_read_only_attrs_through_setitem(self):
 
 762         # Test that we are unable to set readonly attrs through __init__
 
 763         for readonly_key in Task.read_only_fields:
 
 764             t = Task(self.tw, description='test task')
 
 767                 lambda: t.__setitem__(readonly_key, 'value'),
 
 770     def test_saving_unmodified_task(self):
 
 771         t = Task(self.tw, description='test task')
 
 775     def test_adding_tag_by_appending(self):
 
 776         t = Task(self.tw, description='test task', tags=['test1'])
 
 778         t['tags'].add('test2')
 
 780         self.assertEqual(t['tags'], set(['test1', 'test2']))
 
 782     def test_adding_tag_twice(self):
 
 783         t = Task(self.tw, description='test task', tags=['test1'])
 
 785         t['tags'].add('test2')
 
 786         t['tags'].add('test2')
 
 788         self.assertEqual(t['tags'], set(['test1', 'test2']))
 
 790     def test_adding_tag_by_appending_empty(self):
 
 791         t = Task(self.tw, description='test task')
 
 793         t['tags'].add('test')
 
 795         self.assertEqual(t['tags'], set(['test']))
 
 797     def test_serializers_returning_empty_string_for_none(self):
 
 798         # Test that any serializer returns '' when passed None
 
 801             getattr(t, serializer_name)
 
 802             for serializer_name in filter(
 
 803                 lambda x: x.startswith('serialize_'),
 
 807         for serializer in serializers:
 
 808             self.assertEqual(serializer(None), '')
 
 810     def test_deserializer_returning_empty_value_for_empty_string(self):
 
 811         # Test that any deserializer returns empty value when passed ''
 
 814             getattr(t, deserializer_name)
 
 815             for deserializer_name in filter(
 
 816                 lambda x: x.startswith('deserialize_'),
 
 820         for deserializer in deserializers:
 
 821             self.assertTrue(deserializer('') in (None, [], set()))
 
 823     def test_normalizers_handling_none(self):
 
 824         # Test that any normalizer can handle None as a valid value
 
 827         for key in TASK_STANDARD_ATTRS:
 
 828             t._normalize(key, None)
 
 830     def test_recurrent_task_generation(self):
 
 831         today = datetime.date.today()
 
 834             description='brush teeth',
 
 839         self.assertEqual(len(self.tw.tasks.pending()), 2)
 
 841     def test_spawned_task_parent(self):
 
 842         today = datetime.date.today()
 
 845             description='brush teeth',
 
 851         spawned = self.tw.tasks.pending().get(due=today)
 
 852         assert spawned['parent'] == t
 
 854     def test_modify_number_of_tasks_at_once(self):
 
 855         for i in range(1, 100):
 
 856             Task(self.tw, description='test task %d' % i, tags=['test']).save()
 
 858         self.tw.execute_command(['+test', 'mod', 'unified', 'description'])
 
 860     def test_return_all_from_executed_command(self):
 
 861         Task(self.tw, description='test task', tags=['test']).save()
 
 862         out, err, rc = self.tw.execute_command(['count'], return_all=True)
 
 863         self.assertEqual(rc, 0)
 
 865     def test_return_all_from_failed_executed_command(self):
 
 866         Task(self.tw, description='test task', tags=['test']).save()
 
 867         out, err, rc = self.tw.execute_command(
 
 872         self.assertNotEqual(rc, 0)
 
 875 class TaskFromHookTest(TasklibTest):
 
 877     input_add_data = six.StringIO(
 
 878         '{"description":"Buy some milk",'
 
 879         '"entry":"20141118T050231Z",'
 
 880         '"status":"pending",'
 
 881         '"start":"20141119T152233Z",'
 
 882         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}',
 
 885     input_add_data_recurring = six.StringIO(
 
 886         '{"description":"Mow the lawn",'
 
 887         '"entry":"20160210T224304Z",'
 
 888         '"parent":"62da6227-519c-42c2-915d-dccada926ad7",'
 
 890         '"status":"pending",'
 
 891         '"uuid":"81305335-0237-49ff-8e87-b3cdc2369cec"}',
 
 894     input_modify_data = six.StringIO(
 
 896             input_add_data.getvalue(),
 
 898                 '{"description":"Buy some milk finally",'
 
 899                 '"entry":"20141118T050231Z",'
 
 900                 '"status":"completed",'
 
 901                 '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}'
 
 906     exported_raw_data = (
 
 908         '"due":"20150101T232323Z",'
 
 909         '"description":"test task"}'
 
 912     def test_setting_up_from_add_hook_input(self):
 
 913         t = Task.from_input(input_file=self.input_add_data, backend=self.tw)
 
 914         self.assertEqual(t['description'], 'Buy some milk')
 
 915         self.assertEqual(t.pending, True)
 
 917     def test_setting_up_from_add_hook_input_recurring(self):
 
 919             input_file=self.input_add_data_recurring,
 
 922         self.assertEqual(t['description'], 'Mow the lawn')
 
 923         self.assertEqual(t.pending, True)
 
 925     def test_setting_up_from_modified_hook_input(self):
 
 927             input_file=self.input_modify_data,
 
 931         self.assertEqual(t['description'], 'Buy some milk finally')
 
 932         self.assertEqual(t.pending, False)
 
 933         self.assertEqual(t.completed, True)
 
 935         self.assertEqual(t._original_data['status'], 'pending')
 
 936         self.assertEqual(t._original_data['description'], 'Buy some milk')
 
 938             set(t._modified_fields),
 
 939             set(['status', 'description', 'start']),
 
 942     def test_export_data(self):
 
 945             description='test task',
 
 947             due=pytz.utc.localize(
 
 948                 datetime.datetime(2015, 1, 1, 23, 23, 23)),
 
 951         # Check that the output is a permutation of:
 
 952         # {"project":"Home","description":"test task","due":"20150101232323Z"}
 
 953         allowed_segments = self.exported_raw_data[1:-1].split(',')
 
 955             '{' + ','.join(segments) + '}'
 
 956             for segments in itertools.permutations(allowed_segments)
 
 960             any(t.export_data() == expected
 
 961                 for expected in allowed_output),
 
 965 class TimezoneAwareDatetimeTest(TasklibTest):
 
 968         super(TimezoneAwareDatetimeTest, self).setUp()
 
 969         self.zone = local_zone
 
 970         self.localdate_naive = datetime.datetime(2015, 2, 2)
 
 971         self.localtime_naive = datetime.datetime(2015, 2, 2, 0, 0, 0)
 
 972         self.localtime_aware = self.zone.localize(self.localtime_naive)
 
 973         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
 
 975     def test_timezone_naive_datetime_setitem(self):
 
 976         t = Task(self.tw, description='test task')
 
 977         t['due'] = self.localtime_naive
 
 978         self.assertEqual(t['due'], self.localtime_aware)
 
 980     def test_timezone_naive_datetime_using_init(self):
 
 981         t = Task(self.tw, description='test task', due=self.localtime_naive)
 
 982         self.assertEqual(t['due'], self.localtime_aware)
 
 984     def test_filter_by_naive_datetime(self):
 
 985         t = Task(self.tw, description='task1', due=self.localtime_naive)
 
 987         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
 
 988         self.assertEqual(len(matching_tasks), 1)
 
 990     def test_serialize_naive_datetime(self):
 
 991         t = Task(self.tw, description='task1', due=self.localtime_naive)
 
 993             json.loads(t.export_data())['due'],
 
 994             self.utctime_aware.strftime(DATE_FORMAT),
 
 997     def test_timezone_naive_date_setitem(self):
 
 998         t = Task(self.tw, description='test task')
 
 999         t['due'] = self.localdate_naive
 
1000         self.assertEqual(t['due'], self.localtime_aware)
 
1002     def test_timezone_naive_date_using_init(self):
 
1003         t = Task(self.tw, description='test task', due=self.localdate_naive)
 
1004         self.assertEqual(t['due'], self.localtime_aware)
 
1006     def test_filter_by_naive_date(self):
 
1007         t = Task(self.tw, description='task1', due=self.localdate_naive)
 
1009         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
 
1010         self.assertEqual(len(matching_tasks), 1)
 
1012     def test_serialize_naive_date(self):
 
1013         t = Task(self.tw, description='task1', due=self.localdate_naive)
 
1015             json.loads(t.export_data())['due'],
 
1016             self.utctime_aware.strftime(DATE_FORMAT),
 
1019     def test_timezone_aware_datetime_setitem(self):
 
1020         t = Task(self.tw, description='test task')
 
1021         t['due'] = self.localtime_aware
 
1022         self.assertEqual(t['due'], self.localtime_aware)
 
1024     def test_timezone_aware_datetime_using_init(self):
 
1025         t = Task(self.tw, description='test task', due=self.localtime_aware)
 
1026         self.assertEqual(t['due'], self.localtime_aware)
 
1028     def test_filter_by_aware_datetime(self):
 
1029         t = Task(self.tw, description='task1', due=self.localtime_aware)
 
1031         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
 
1032         self.assertEqual(len(matching_tasks), 1)
 
1034     def test_serialize_aware_datetime(self):
 
1035         t = Task(self.tw, description='task1', due=self.localtime_aware)
 
1037             json.loads(t.export_data())['due'],
 
1038             self.utctime_aware.strftime(DATE_FORMAT),
 
1042 class DatetimeStringTest(TasklibTest):
 
1044     def test_simple_now_conversion(self):
 
1045         if self.tw.version < six.text_type('2.4.0'):
 
1046             # Python2.6 does not support SkipTest. As a workaround
 
1047             # mark the test as passed by exiting.
 
1048             if getattr(unittest, 'SkipTest', None) is not None:
 
1049                 raise unittest.SkipTest()
 
1053         t = Task(self.tw, description='test task', due='now')
 
1054         now = local_zone.localize(datetime.datetime.now())
 
1056         # Assert that both times are not more than 5 seconds apart
 
1057         if sys.version_info < (2, 7):
 
1058             self.assertTrue(total_seconds_2_6(now - t['due']) < 5)
 
1059             self.assertTrue(total_seconds_2_6(t['due'] - now) < 5)
 
1061             self.assertTrue((now - t['due']).total_seconds() < 5)
 
1062             self.assertTrue((t['due'] - now).total_seconds() < 5)
 
1064     def test_simple_eoy_conversion(self):
 
1065         if self.tw.version < six.text_type('2.4.0'):
 
1066             # Python2.6 does not support SkipTest. As a workaround
 
1067             # mark the test as passed by exiting.
 
1068             if getattr(unittest, 'SkipTest', None) is not None:
 
1069                 raise unittest.SkipTest()
 
1073         t = Task(self.tw, description='test task', due='eoy')
 
1074         now = local_zone.localize(datetime.datetime.now())
 
1075         eoy = local_zone.localize(datetime.datetime(
 
1083         self.assertEqual(eoy, t['due'])
 
1085     def test_complex_eoy_conversion(self):
 
1086         if self.tw.version < six.text_type('2.4.0'):
 
1087             # Python2.6 does not support SkipTest. As a workaround
 
1088             # mark the test as passed by exiting.
 
1089             if getattr(unittest, 'SkipTest', None) is not None:
 
1090                 raise unittest.SkipTest()
 
1094         t = Task(self.tw, description='test task', due='eoy - 4 months')
 
1095         now = local_zone.localize(datetime.datetime.now())
 
1096         due_date = local_zone.localize(
 
1105         ) - datetime.timedelta(0, 4 * 30 * 86400)
 
1106         self.assertEqual(due_date, t['due'])
 
1108     def test_filtering_with_string_datetime(self):
 
1109         if self.tw.version < six.text_type('2.4.0'):
 
1110             # Python2.6 does not support SkipTest. As a workaround
 
1111             # mark the test as passed by exiting.
 
1112             if getattr(unittest, 'SkipTest', None) is not None:
 
1113                 raise unittest.SkipTest()
 
1119             description='test task',
 
1120             due=datetime.datetime.now() - datetime.timedelta(0, 2),
 
1123         self.assertEqual(len(self.tw.tasks.filter(due__before='now')), 1)
 
1126 class AnnotationTest(TasklibTest):
 
1129         super(AnnotationTest, self).setUp()
 
1130         Task(self.tw, description='test task').save()
 
1132     def test_adding_annotation(self):
 
1133         task = self.tw.tasks.get()
 
1134         task.add_annotation('test annotation')
 
1135         self.assertEqual(len(task['annotations']), 1)
 
1136         ann = task['annotations'][0]
 
1137         self.assertEqual(ann['description'], 'test annotation')
 
1139     def test_removing_annotation(self):
 
1140         task = self.tw.tasks.get()
 
1141         task.add_annotation('test annotation')
 
1142         ann = task['annotations'][0]
 
1144         self.assertEqual(len(task['annotations']), 0)
 
1146     def test_removing_annotation_by_description(self):
 
1147         task = self.tw.tasks.get()
 
1148         task.add_annotation('test annotation')
 
1149         task.remove_annotation('test annotation')
 
1150         self.assertEqual(len(task['annotations']), 0)
 
1152     def test_removing_annotation_by_obj(self):
 
1153         task = self.tw.tasks.get()
 
1154         task.add_annotation('test annotation')
 
1155         ann = task['annotations'][0]
 
1156         task.remove_annotation(ann)
 
1157         self.assertEqual(len(task['annotations']), 0)
 
1159     def test_annotation_after_modification(self):
 
1160         task = self.tw.tasks.get()
 
1161         task['project'] = 'test'
 
1162         task.add_annotation('I should really do this task')
 
1163         self.assertEqual(task['project'], 'test')
 
1165         self.assertEqual(task['project'], 'test')
 
1167     def test_serialize_annotations(self):
 
1168         # Test that serializing annotations is possible
 
1169         t = Task(self.tw, description='test')
 
1172         t.add_annotation('annotation1')
 
1173         t.add_annotation('annotation2')
 
1175         data = t._serialize('annotations', t._data['annotations'])
 
1177         self.assertEqual(len(data), 2)
 
1178         self.assertEqual(type(data[0]), dict)
 
1179         self.assertEqual(type(data[1]), dict)
 
1181         self.assertEqual(data[0]['description'], 'annotation1')
 
1182         self.assertEqual(data[1]['description'], 'annotation2')
 
1185 class UnicodeTest(TasklibTest):
 
1187     def test_unicode_task(self):
 
1188         Task(self.tw, description=six.u('†åßk')).save()
 
1191     def test_filter_by_unicode_task(self):
 
1192         Task(self.tw, description=six.u('†åßk')).save()
 
1193         tasks = self.tw.tasks.filter(description=six.u('†åßk'))
 
1194         self.assertEqual(len(tasks), 1)
 
1196     def test_non_unicode_task(self):
 
1197         Task(self.tw, description='test task').save()
 
1201 class ReadOnlyDictViewTest(unittest.TestCase):
 
1204         self.sample = dict(sample_list=[1, 2, 3], sample_dict={'key': 'value'})
 
1205         self.original_sample = copy.deepcopy(self.sample)
 
1206         self.view = ReadOnlyDictView(self.sample)
 
1208     def test_readonlydictview_getitem(self):
 
1209         sample_list = self.view['sample_list']
 
1210         self.assertEqual(sample_list, self.sample['sample_list'])
 
1212         # Assert that modification changed only copied value
 
1213         sample_list.append(4)
 
1214         self.assertNotEqual(sample_list, self.sample['sample_list'])
 
1216         # Assert that viewed dict is not changed
 
1217         self.assertEqual(self.sample, self.original_sample)
 
1219     def test_readonlydictview_contains(self):
 
1220         self.assertEqual('sample_list' in self.view,
 
1221                          'sample_list' in self.sample)
 
1222         self.assertEqual('sample_dict' in self.view,
 
1223                          'sample_dict' in self.sample)
 
1224         self.assertEqual('key' in self.view, 'key' in self.sample)
 
1226         # Assert that viewed dict is not changed
 
1227         self.assertEqual(self.sample, self.original_sample)
 
1229     def test_readonlydictview_iter(self):
 
1231             list(key for key in self.view),
 
1232             list(key for key in self.sample),
 
1235         # Assert the view is correct after modification
 
1236         self.sample['new'] = 'value'
 
1238             list(key for key in self.view),
 
1239             list(key for key in self.sample),
 
1242     def test_readonlydictview_len(self):
 
1243         self.assertEqual(len(self.view), len(self.sample))
 
1245         # Assert the view is correct after modification
 
1246         self.sample['new'] = 'value'
 
1247         self.assertEqual(len(self.view), len(self.sample))
 
1249     def test_readonlydictview_get(self):
 
1250         sample_list = self.view.get('sample_list')
 
1251         self.assertEqual(sample_list, self.sample.get('sample_list'))
 
1253         # Assert that modification changed only copied value
 
1254         sample_list.append(4)
 
1255         self.assertNotEqual(sample_list, self.sample.get('sample_list'))
 
1257         # Assert that viewed dict is not changed
 
1258         self.assertEqual(self.sample, self.original_sample)
 
1260     def test_readonlydict_items(self):
 
1261         view_items = self.view.items()
 
1262         sample_items = list(self.sample.items())
 
1263         self.assertEqual(view_items, sample_items)
 
1265         view_items.append('newkey')
 
1266         self.assertNotEqual(view_items, sample_items)
 
1267         self.assertEqual(self.sample, self.original_sample)
 
1269     def test_readonlydict_values(self):
 
1270         view_values = self.view.values()
 
1271         sample_values = list(self.sample.values())
 
1272         self.assertEqual(view_values, sample_values)
 
1274         view_list_item = list(filter(lambda x: type(x) is list,
 
1276         view_list_item.append(4)
 
1277         self.assertNotEqual(view_values, sample_values)
 
1278         self.assertEqual(self.sample, self.original_sample)
 
1281 class LazyUUIDTaskTest(TasklibTest):
 
1284         super(LazyUUIDTaskTest, self).setUp()
 
1286         self.stored = Task(self.tw, description='this is test task')
 
1289         self.lazy = LazyUUIDTask(self.tw, self.stored['uuid'])
 
1291     def test_uuid_non_conversion(self):
 
1292         assert self.stored['uuid'] == self.lazy['uuid']
 
1293         assert type(self.lazy) is LazyUUIDTask
 
1295     def test_lazy_explicit_conversion(self):
 
1296         assert type(self.lazy) is LazyUUIDTask
 
1298         assert type(self.lazy) is Task
 
1300     def test_conversion_key(self):
 
1301         assert self.stored['description'] == self.lazy['description']
 
1302         assert type(self.lazy) is Task
 
1304     def test_conversion_attribute(self):
 
1305         assert type(self.lazy) is LazyUUIDTask
 
1306         assert self.lazy.completed is False
 
1307         assert type(self.lazy) is Task
 
1309     def test_normal_to_lazy_equality(self):
 
1310         assert self.stored == self.lazy
 
1311         assert not self.stored != self.lazy
 
1312         assert type(self.lazy) is LazyUUIDTask
 
1314     def test_lazy_to_lazy_equality(self):
 
1315         lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
 
1316         lazy2 = LazyUUIDTask(self.tw, self.stored['uuid'])
 
1318         assert lazy1 == lazy2
 
1319         assert not lazy1 != lazy2
 
1320         assert type(lazy1) is LazyUUIDTask
 
1321         assert type(lazy2) is LazyUUIDTask
 
1323     def test_normal_to_lazy_inequality(self):
 
1324         # Create a different UUID by changing the last letter
 
1325         wrong_uuid = self.stored['uuid']
 
1326         wrong_uuid = wrong_uuid[:-1] + ('a' if wrong_uuid[-1] != 'a' else 'b')
 
1328         wrong_lazy = LazyUUIDTask(self.tw, wrong_uuid)
 
1330         assert not self.stored == wrong_lazy
 
1331         assert self.stored != wrong_lazy
 
1332         assert type(wrong_lazy) is LazyUUIDTask
 
1334     def test_lazy_to_lazy_inequality(self):
 
1335         # Create a different UUID by changing the last letter
 
1336         wrong_uuid = self.stored['uuid']
 
1337         wrong_uuid = wrong_uuid[:-1] + ('a' if wrong_uuid[-1] != 'a' else 'b')
 
1339         lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
 
1340         lazy2 = LazyUUIDTask(self.tw, wrong_uuid)
 
1342         assert not lazy1 == lazy2
 
1343         assert lazy1 != lazy2
 
1344         assert type(lazy1) is LazyUUIDTask
 
1345         assert type(lazy2) is LazyUUIDTask
 
1347     def test_lazy_in_queryset(self):
 
1348         tasks = self.tw.tasks.filter(uuid=self.stored['uuid'])
 
1350         assert self.lazy in tasks
 
1351         assert type(self.lazy) is LazyUUIDTask
 
1353     def test_lazy_saved(self):
 
1354         assert self.lazy.saved is True
 
1356     def test_lazy_modified(self):
 
1357         assert self.lazy.modified is False
 
1359     def test_lazy_modified_fields(self):
 
1360         assert self.lazy._modified_fields == set()
 
1363 class LazyUUIDTaskSetTest(TasklibTest):
 
1366         super(LazyUUIDTaskSetTest, self).setUp()
 
1368         self.task1 = Task(self.tw, description='task 1')
 
1369         self.task2 = Task(self.tw, description='task 2')
 
1370         self.task3 = Task(self.tw, description='task 3')
 
1382         self.lazy = LazyUUIDTaskSet(self.tw, self.uuids)
 
1384     def test_length(self):
 
1385         assert len(self.lazy) == 3
 
1386         assert type(self.lazy) is LazyUUIDTaskSet
 
1388     def test_contains(self):
 
1389         assert self.task1 in self.lazy
 
1390         assert self.task2 in self.lazy
 
1391         assert self.task3 in self.lazy
 
1392         assert type(self.lazy) is LazyUUIDTaskSet
 
1394     def test_eq_lazy(self):
 
1395         new_lazy = LazyUUIDTaskSet(self.tw, self.uuids)
 
1396         assert self.lazy == new_lazy
 
1397         assert not self.lazy != new_lazy
 
1398         assert type(self.lazy) is LazyUUIDTaskSet
 
1400     def test_eq_real(self):
 
1401         assert self.lazy == self.tw.tasks.all()
 
1402         assert self.tw.tasks.all() == self.lazy
 
1403         assert not self.lazy != self.tw.tasks.all()
 
1405         assert type(self.lazy) is LazyUUIDTaskSet
 
1407     def test_union(self):
 
1408         taskset = set([self.task1])
 
1409         lazyset = LazyUUIDTaskSet(
 
1411             (self.task2['uuid'], self.task3['uuid']),
 
1414         assert taskset | lazyset == self.lazy
 
1415         assert lazyset | taskset == self.lazy
 
1416         assert taskset.union(lazyset) == self.lazy
 
1417         assert lazyset.union(taskset) == self.lazy
 
1420         assert lazyset == self.lazy
 
1422     def test_difference(self):
 
1423         taskset = set([self.task1, self.task2])
 
1424         lazyset = LazyUUIDTaskSet(
 
1426             (self.task2['uuid'], self.task3['uuid']),
 
1429         assert taskset - lazyset == set([self.task1])
 
1430         assert lazyset - taskset == set([self.task3])
 
1431         assert taskset.difference(lazyset) == set([self.task1])
 
1432         assert lazyset.difference(taskset) == set([self.task3])
 
1435         assert lazyset == set([self.task3])
 
1437     def test_symmetric_difference(self):
 
1438         taskset = set([self.task1, self.task2])
 
1439         lazyset = LazyUUIDTaskSet(
 
1441             (self.task2['uuid'], self.task3['uuid']),
 
1444         assert taskset ^ lazyset == set([self.task1, self.task3])
 
1445         assert lazyset ^ taskset == set([self.task1, self.task3])
 
1447             taskset.symmetric_difference(lazyset),
 
1448             set([self.task1, self.task3]),
 
1451             lazyset.symmetric_difference(taskset),
 
1452             set([self.task1, self.task3]),
 
1456         assert lazyset == set([self.task1, self.task3])
 
1458     def test_intersection(self):
 
1459         taskset = set([self.task1, self.task2])
 
1460         lazyset = LazyUUIDTaskSet(
 
1462             (self.task2['uuid'], self.task3['uuid']),
 
1465         assert taskset & lazyset == set([self.task2])
 
1466         assert lazyset & taskset == set([self.task2])
 
1467         assert taskset.intersection(lazyset) == set([self.task2])
 
1468         assert lazyset.intersection(taskset) == set([self.task2])
 
1471         assert lazyset == set([self.task2])
 
1474 class TaskWarriorBackendTest(TasklibTest):
 
1476     def test_config(self):
 
1477         assert self.tw.config['nag'] == 'You have more urgent tasks.'
 
1478         assert self.tw.config['default.command'] == 'next'
 
1479         assert self.tw.config['dependency.indicator'] == 'D'