]> git.madduck.net Git - etc/taskwarrior.git/blob - tasklib/tests.py

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

tests: Do not use string-based dates for compatibility reasons
[etc/taskwarrior.git] / tasklib / tests.py
1 # coding=utf-8
2
3 import copy
4 import datetime
5 import itertools
6 import json
7 import pytz
8 import six
9 import shutil
10 import sys
11 import tempfile
12 import unittest
13
14 from .backends import TaskWarrior
15 from .task import Task, ReadOnlyDictView, TaskQuerySet
16 from .lazy import LazyUUIDTask, LazyUUIDTaskSet
17 from .serializing import DATE_FORMAT, local_zone
18
19 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
20 TASK_STANDARD_ATTRS = (
21     'status',
22     'uuid',
23     'entry',
24     'description',
25     'start',
26     'end',
27     'due',
28     'until',
29     'wait',
30     'modified',
31     'scheduled',
32     'recur',
33     'mask',
34     'imask',
35     'parent',
36     'project',
37     'priority',
38     'depends',
39     'tags',
40     'annotations',
41 )
42
43 total_seconds_2_6 = lambda x: x.microseconds / 1e6 + x.seconds + x.days * 24 * 3600
44
45
46 class TasklibTest(unittest.TestCase):
47
48     def setUp(self):
49         self.tmp = tempfile.mkdtemp(dir='.')
50         self.tw = TaskWarrior(data_location=self.tmp, taskrc_location='/')
51
52     def tearDown(self):
53         shutil.rmtree(self.tmp)
54
55
56 class TaskFilterTest(TasklibTest):
57
58     def test_all_empty(self):
59         self.assertEqual(len(self.tw.tasks.all()), 0)
60
61     def test_all_non_empty(self):
62         Task(self.tw, description="test task").save()
63         self.assertEqual(len(self.tw.tasks.all()), 1)
64         self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
65         self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
66
67     def test_pending_non_empty(self):
68         Task(self.tw, description="test task").save()
69         self.assertEqual(len(self.tw.tasks.pending()), 1)
70         self.assertEqual(self.tw.tasks.pending()[0]['description'],
71                          'test task')
72         self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
73
74     def test_completed_empty(self):
75         Task(self.tw, description="test task").save()
76         self.assertEqual(len(self.tw.tasks.completed()), 0)
77
78     def test_completed_non_empty(self):
79         Task(self.tw, description="test task").save()
80         self.assertEqual(len(self.tw.tasks.completed()), 0)
81         self.tw.tasks.all()[0].done()
82         self.assertEqual(len(self.tw.tasks.completed()), 1)
83
84     def test_deleted_empty(self):
85         Task(self.tw, description="test task").save()
86         self.assertEqual(len(self.tw.tasks.deleted()), 0)
87
88     def test_deleted_non_empty(self):
89         Task(self.tw, description="test task").save()
90         self.assertEqual(len(self.tw.tasks.deleted()), 0)
91         self.tw.tasks.all()[0].delete()
92         self.assertEqual(len(self.tw.tasks.deleted()), 1)
93
94     def test_waiting_empty(self):
95         Task(self.tw, description="test task").save()
96         self.assertEqual(len(self.tw.tasks.waiting()), 0)
97
98     def test_waiting_non_empty(self):
99         Task(self.tw, description="test task").save()
100         self.assertEqual(len(self.tw.tasks.waiting()), 0)
101
102         t = self.tw.tasks.all()[0]
103         t['wait'] = datetime.datetime.now() + datetime.timedelta(days=1)
104         t.save()
105
106         self.assertEqual(len(self.tw.tasks.waiting()), 1)
107
108     def test_filtering_by_attribute(self):
109         Task(self.tw, description="no priority task").save()
110         Task(self.tw, priority="H", description="high priority task").save()
111         self.assertEqual(len(self.tw.tasks.all()), 2)
112
113         # Assert that the correct number of tasks is returned
114         self.assertEqual(len(self.tw.tasks.filter(priority="H")), 1)
115
116         # Assert that the correct tasks are returned
117         high_priority_task = self.tw.tasks.get(priority="H")
118         self.assertEqual(high_priority_task['description'], "high priority task")
119
120     def test_filtering_by_empty_attribute(self):
121         Task(self.tw, description="no priority task").save()
122         Task(self.tw, priority="H", description="high priority task").save()
123         self.assertEqual(len(self.tw.tasks.all()), 2)
124
125         # Assert that the correct number of tasks is returned
126         self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
127
128         # Assert that the correct tasks are returned
129         no_priority_task = self.tw.tasks.get(priority=None)
130         self.assertEqual(no_priority_task['description'], "no priority task")
131
132     def test_filter_for_task_with_space_in_descripition(self):
133         task = Task(self.tw, description="test task")
134         task.save()
135
136         filtered_task = self.tw.tasks.get(description="test task")
137         self.assertEqual(filtered_task['description'], "test task")
138
139     def test_filter_for_task_without_space_in_descripition(self):
140         task = Task(self.tw, description="test")
141         task.save()
142
143         filtered_task = self.tw.tasks.get(description="test")
144         self.assertEqual(filtered_task['description'], "test")
145
146     def test_filter_for_task_with_space_in_project(self):
147         task = Task(self.tw, description="test", project="random project")
148         task.save()
149
150         filtered_task = self.tw.tasks.get(project="random project")
151         self.assertEqual(filtered_task['project'], "random project")
152
153     def test_filter_for_task_without_space_in_project(self):
154         task = Task(self.tw, description="test", project="random")
155         task.save()
156
157         filtered_task = self.tw.tasks.get(project="random")
158         self.assertEqual(filtered_task['project'], "random")
159
160     def test_filter_with_empty_uuid(self):
161         self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid=''))
162
163     def test_filter_dummy_by_status(self):
164         t = Task(self.tw, description="test")
165         t.save()
166
167         tasks = self.tw.tasks.filter(status=t['status'])
168         self.assertEqual(list(tasks), [t])
169
170     def test_filter_dummy_by_uuid(self):
171         t = Task(self.tw, description="test")
172         t.save()
173
174         tasks = self.tw.tasks.filter(uuid=t['uuid'])
175         self.assertEqual(list(tasks), [t])
176
177     def test_filter_dummy_by_entry(self):
178         t = Task(self.tw, description="test")
179         t.save()
180
181         tasks = self.tw.tasks.filter(entry=t['entry'])
182         self.assertEqual(list(tasks), [t])
183
184     def test_filter_dummy_by_description(self):
185         t = Task(self.tw, description="test")
186         t.save()
187
188         tasks = self.tw.tasks.filter(description=t['description'])
189         self.assertEqual(list(tasks), [t])
190
191     def test_filter_dummy_by_start(self):
192         t = Task(self.tw, description="test")
193         t.save()
194         t.start()
195
196         tasks = self.tw.tasks.filter(start=t['start'])
197         self.assertEqual(list(tasks), [t])
198
199     def test_filter_dummy_by_end(self):
200         t = Task(self.tw, description="test")
201         t.save()
202         t.done()
203
204         tasks = self.tw.tasks.filter(end=t['end'])
205         self.assertEqual(list(tasks), [t])
206
207     def test_filter_dummy_by_due(self):
208         t = Task(self.tw, description="test", due=datetime.datetime.now())
209         t.save()
210
211         tasks = self.tw.tasks.filter(due=t['due'])
212         self.assertEqual(list(tasks), [t])
213
214     def test_filter_dummy_by_until(self):
215         t = Task(self.tw, description="test")
216         t.save()
217
218         tasks = self.tw.tasks.filter(until=t['until'])
219         self.assertEqual(list(tasks), [t])
220
221     def test_filter_dummy_by_modified(self):
222         # Older TW version does not support bumping modified
223         # on save
224         if self.tw.version < six.text_type('2.2.0'):
225             # Python2.6 does not support SkipTest. As a workaround
226             # mark the test as passed by exiting.
227             if getattr(unittest, 'SkipTest', None) is not None:
228                 raise unittest.SkipTest()
229             else:
230                 return
231
232         t = Task(self.tw, description="test")
233         t.save()
234
235         tasks = self.tw.tasks.filter(modified=t['modified'])
236         self.assertEqual(list(tasks), [t])
237
238     def test_filter_dummy_by_scheduled(self):
239         t = Task(self.tw, description="test")
240         t.save()
241
242         tasks = self.tw.tasks.filter(scheduled=t['scheduled'])
243         self.assertEqual(list(tasks), [t])
244
245     def test_filter_dummy_by_tags(self):
246         t = Task(self.tw, description="test", tags=["home"])
247         t.save()
248
249         tasks = self.tw.tasks.filter(tags=t['tags'])
250         self.assertEqual(list(tasks), [t])
251
252     def test_filter_dummy_by_projects(self):
253         t = Task(self.tw, description="test", project="random")
254         t.save()
255
256         tasks = self.tw.tasks.filter(project=t['project'])
257         self.assertEqual(list(tasks), [t])
258
259     def test_filter_by_priority(self):
260         t = Task(self.tw, description="test", priority="H")
261         t.save()
262
263         tasks = self.tw.tasks.filter(priority=t['priority'])
264         self.assertEqual(list(tasks), [t])
265
266
267 class TaskTest(TasklibTest):
268
269     def test_create_unsaved_task(self):
270         # Make sure a new task is not saved unless explicitly called for
271         t = Task(self.tw, description="test task")
272         self.assertEqual(len(self.tw.tasks.all()), 0)
273
274     # TODO: once python 2.6 compatiblity is over, use context managers here
275     #       and in all subsequent tests for assertRaises
276
277     def test_delete_unsaved_task(self):
278         t = Task(self.tw, description="test task")
279         self.assertRaises(Task.NotSaved, t.delete)
280
281     def test_complete_unsaved_task(self):
282         t = Task(self.tw, description="test task")
283         self.assertRaises(Task.NotSaved, t.done)
284
285     def test_refresh_unsaved_task(self):
286         t = Task(self.tw, description="test task")
287         self.assertRaises(Task.NotSaved, t.refresh)
288
289     def test_start_unsaved_task(self):
290         t = Task(self.tw, description="test task")
291         self.assertRaises(Task.NotSaved, t.start)
292
293     def test_delete_deleted_task(self):
294         t = Task(self.tw, description="test task")
295         t.save()
296         t.delete()
297
298         self.assertRaises(Task.DeletedTask, t.delete)
299
300     def test_complete_completed_task(self):
301         t = Task(self.tw, description="test task")
302         t.save()
303         t.done()
304
305         self.assertRaises(Task.CompletedTask, t.done)
306
307     def test_start_completed_task(self):
308         t = Task(self.tw, description="test task")
309         t.save()
310         t.done()
311
312         self.assertRaises(Task.CompletedTask, t.start)
313
314     def test_add_completed_task(self):
315         t = Task(self.tw, description="test", status="completed",
316                  end=datetime.datetime.now())
317         t.save()
318
319     def test_add_multiple_completed_tasks(self):
320         t1 = Task(self.tw, description="test1", status="completed",
321                  end=datetime.datetime.now())
322         t2 = Task(self.tw, description="test2", status="completed",
323                  end=datetime.datetime.now())
324         t1.save()
325         t2.save()
326
327     def test_complete_deleted_task(self):
328         t = Task(self.tw, description="test task")
329         t.save()
330         t.delete()
331
332         self.assertRaises(Task.DeletedTask, t.done)
333
334     def test_starting_task(self):
335         t = Task(self.tw, description="test task")
336         now = t.datetime_normalizer(datetime.datetime.now())
337         t.save()
338         t.start()
339
340         self.assertTrue(now.replace(microsecond=0) <= t['start'])
341         self.assertEqual(t['status'], 'pending')
342
343     def test_completing_task(self):
344         t = Task(self.tw, description="test task")
345         now = t.datetime_normalizer(datetime.datetime.now())
346         t.save()
347         t.done()
348
349         self.assertTrue(now.replace(microsecond=0) <= t['end'])
350         self.assertEqual(t['status'], 'completed')
351
352     def test_deleting_task(self):
353         t = Task(self.tw, description="test task")
354         now = t.datetime_normalizer(datetime.datetime.now())
355         t.save()
356         t.delete()
357
358         self.assertTrue(now.replace(microsecond=0) <= t['end'])
359         self.assertEqual(t['status'], 'deleted')
360
361     def test_started_task_active(self):
362         t = Task(self.tw, description="test task")
363         t.save()
364         t.start()
365         self.assertTrue(t.active)
366
367     def test_unstarted_task_inactive(self):
368         t = Task(self.tw, description="test task")
369         self.assertFalse(t.active)
370         t.save()
371         self.assertFalse(t.active)
372
373     def test_start_active_task(self):
374         t = Task(self.tw, description="test task")
375         t.save()
376         t.start()
377         self.assertRaises(Task.ActiveTask, t.start)
378
379     def test_stop_completed_task(self):
380         t = Task(self.tw, description="test task")
381         t.save()
382         t.start()
383         t.done()
384
385         self.assertRaises(Task.InactiveTask, t.stop)
386
387         t = Task(self.tw, description="test task")
388         t.save()
389         t.done()
390
391         self.assertRaises(Task.InactiveTask, t.stop)
392
393     def test_stop_deleted_task(self):
394         t = Task(self.tw, description="test task")
395         t.save()
396         t.start()
397         t.delete()
398         t.stop()
399
400     def test_stop_inactive_task(self):
401         t = Task(self.tw, description="test task")
402         t.save()
403
404         self.assertRaises(Task.InactiveTask, t.stop)
405
406         t = Task(self.tw, description="test task")
407         t.save()
408         t.start()
409         t.stop()
410
411         self.assertRaises(Task.InactiveTask, t.stop)
412
413     def test_stopping_task(self):
414         t = Task(self.tw, description="test task")
415         now = t.datetime_normalizer(datetime.datetime.now())
416         t.save()
417         t.start()
418         t.stop()
419
420         self.assertEqual(t['end'], None)
421         self.assertEqual(t['status'], 'pending')
422         self.assertFalse(t.active)
423
424     def test_modify_simple_attribute_without_space(self):
425         t = Task(self.tw, description="test")
426         t.save()
427
428         self.assertEquals(t['description'], "test")
429
430         t['description'] = "test-modified"
431         t.save()
432
433         self.assertEquals(t['description'], "test-modified")
434
435     def test_modify_simple_attribute_with_space(self):
436         # Space can pose problems with parsing
437         t = Task(self.tw, description="test task")
438         t.save()
439
440         self.assertEquals(t['description'], "test task")
441
442         t['description'] = "test task modified"
443         t.save()
444
445         self.assertEquals(t['description'], "test task modified")
446
447     def test_empty_dependency_set_of_unsaved_task(self):
448         t = Task(self.tw, description="test task")
449         self.assertEqual(t['depends'], set())
450
451     def test_empty_dependency_set_of_saved_task(self):
452         t = Task(self.tw, description="test task")
453         t.save()
454         self.assertEqual(t['depends'], set())
455
456     def test_set_unsaved_task_as_dependency(self):
457         # Adds only one dependency to task with no dependencies
458         t = Task(self.tw, description="test task")
459         dependency = Task(self.tw, description="needs to be done first")
460
461         # We only save the parent task, dependency task is unsaved
462         t.save()
463         t['depends'] = set([dependency])
464
465         self.assertRaises(Task.NotSaved, t.save)
466
467     def test_set_simple_dependency_set(self):
468         # Adds only one dependency to task with no dependencies
469         t = Task(self.tw, description="test task")
470         dependency = Task(self.tw, description="needs to be done first")
471
472         t.save()
473         dependency.save()
474
475         t['depends'] = set([dependency])
476
477         self.assertEqual(t['depends'], set([dependency]))
478
479     def test_set_complex_dependency_set(self):
480         # Adds two dependencies to task with no dependencies
481         t = Task(self.tw, description="test task")
482         dependency1 = Task(self.tw, description="needs to be done first")
483         dependency2 = Task(self.tw, description="needs to be done second")
484
485         t.save()
486         dependency1.save()
487         dependency2.save()
488
489         t['depends'] = set([dependency1, dependency2])
490
491         self.assertEqual(t['depends'], set([dependency1, dependency2]))
492
493     def test_remove_from_dependency_set(self):
494         # Removes dependency from task with two dependencies
495         t = Task(self.tw, description="test task")
496         dependency1 = Task(self.tw, description="needs to be done first")
497         dependency2 = Task(self.tw, description="needs to be done second")
498
499         dependency1.save()
500         dependency2.save()
501
502         t['depends'] = set([dependency1, dependency2])
503         t.save()
504
505         t['depends'].remove(dependency2)
506         t.save()
507
508         self.assertEqual(t['depends'], set([dependency1]))
509
510     def test_add_to_dependency_set(self):
511         # Adds dependency to task with one dependencies
512         t = Task(self.tw, description="test task")
513         dependency1 = Task(self.tw, description="needs to be done first")
514         dependency2 = Task(self.tw, description="needs to be done second")
515
516         dependency1.save()
517         dependency2.save()
518
519         t['depends'] = set([dependency1])
520         t.save()
521
522         t['depends'].add(dependency2)
523         t.save()
524
525         self.assertEqual(t['depends'], set([dependency1, dependency2]))
526
527     def test_add_to_empty_dependency_set(self):
528         # Adds dependency to task with one dependencies
529         t = Task(self.tw, description="test task")
530         dependency = Task(self.tw, description="needs to be done first")
531
532         dependency.save()
533
534         t['depends'].add(dependency)
535         t.save()
536
537         self.assertEqual(t['depends'], set([dependency]))
538
539     def test_simple_dependency_set_save_repeatedly(self):
540         # Adds only one dependency to task with no dependencies
541         t = Task(self.tw, description="test task")
542         dependency = Task(self.tw, description="needs to be done first")
543         dependency.save()
544
545         t['depends'] = set([dependency])
546         t.save()
547
548         # We taint the task, but keep depends intact
549         t['description'] = "test task modified"
550         t.save()
551
552         self.assertEqual(t['depends'], set([dependency]))
553
554         # We taint the task, but assign the same set to the depends
555         t['depends'] = set([dependency])
556         t['description'] = "test task modified again"
557         t.save()
558
559         self.assertEqual(t['depends'], set([dependency]))
560
561     def test_compare_different_tasks(self):
562         # Negative: compare two different tasks
563         t1 = Task(self.tw, description="test task")
564         t2 = Task(self.tw, description="test task")
565
566         t1.save()
567         t2.save()
568
569         self.assertEqual(t1 == t2, False)
570
571     def test_compare_same_task_object(self):
572         # Compare Task object wit itself
573         t = Task(self.tw, description="test task")
574         t.save()
575
576         self.assertEqual(t == t, True)
577
578     def test_compare_same_task(self):
579         # Compare the same task using two different objects
580         t1 = Task(self.tw, description="test task")
581         t1.save()
582
583         t2 = self.tw.tasks.get(uuid=t1['uuid'])
584         self.assertEqual(t1 == t2, True)
585
586     def test_compare_unsaved_tasks(self):
587         # t1 and t2 are unsaved tasks, considered to be unequal
588         # despite the content of data
589         t1 = Task(self.tw, description="test task")
590         t2 = Task(self.tw, description="test task")
591
592         self.assertEqual(t1 == t2, False)
593
594     def test_hash_unsaved_tasks(self):
595         # Considered equal, it's the same object
596         t1 = Task(self.tw, description="test task")
597         t2 = t1
598         self.assertEqual(hash(t1) == hash(t2), True)
599
600     def test_hash_same_task(self):
601         # Compare the hash of the task using two different objects
602         t1 = Task(self.tw, description="test task")
603         t1.save()
604
605         t2 = self.tw.tasks.get(uuid=t1['uuid'])
606         self.assertEqual(t1.__hash__(), t2.__hash__())
607
608     def test_adding_task_with_priority(self):
609         t = Task(self.tw, description="test task", priority="M")
610         t.save()
611
612     def test_removing_priority_with_none(self):
613         t = Task(self.tw, description="test task", priority="L")
614         t.save()
615
616         # Remove the priority mark
617         t['priority'] = None
618         t.save()
619
620         # Assert that priority is not there after saving
621         self.assertEqual(t['priority'], None)
622
623     def test_adding_task_with_due_time(self):
624         t = Task(self.tw, description="test task", due=datetime.datetime.now())
625         t.save()
626
627     def test_removing_due_time_with_none(self):
628         t = Task(self.tw, description="test task", due=datetime.datetime.now())
629         t.save()
630
631         # Remove the due timestamp
632         t['due'] = None
633         t.save()
634
635         # Assert that due timestamp is no longer there
636         self.assertEqual(t['due'], None)
637
638     def test_modified_fields_new_task(self):
639         t = Task(self.tw)
640
641         # This should be empty with new task
642         self.assertEqual(set(t._modified_fields), set())
643
644         # Modify the task
645         t['description'] = "test task"
646         self.assertEqual(set(t._modified_fields), set(['description']))
647
648         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
649         self.assertEqual(set(t._modified_fields), set(['description', 'due']))
650
651         t['project'] = "test project"
652         self.assertEqual(set(t._modified_fields),
653                          set(['description', 'due', 'project']))
654
655         # List of modified fields should clear out when saved
656         t.save()
657         self.assertEqual(set(t._modified_fields), set())
658
659         # Reassigning the fields with the same values now should not produce
660         # modified fields
661         t['description'] = "test task"
662         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
663         t['project'] = "test project"
664         self.assertEqual(set(t._modified_fields), set())
665
666     def test_modified_fields_loaded_task(self):
667         t = Task(self.tw)
668
669         # Modify the task
670         t['description'] = "test task"
671         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
672         t['project'] = "test project"
673
674         dependency = Task(self.tw, description="dependency")
675         dependency.save()
676         t['depends'] = set([dependency])
677
678         # List of modified fields should clear out when saved
679         t.save()
680         self.assertEqual(set(t._modified_fields), set())
681
682         # Get the task by using a filter by UUID
683         t2 = self.tw.tasks.get(uuid=t['uuid'])
684
685         # Reassigning the fields with the same values now should not produce
686         # modified fields
687         t['description'] = "test task"
688         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
689         t['project'] = "test project"
690         t['depends'] = set([dependency])
691         self.assertEqual(set(t._modified_fields), set())
692
693     def test_modified_fields_not_affected_by_reading(self):
694         t = Task(self.tw)
695
696         for field in TASK_STANDARD_ATTRS:
697             value = t[field]
698
699         self.assertEqual(set(t._modified_fields), set())
700
701     def test_setting_read_only_attrs_through_init(self):
702         # Test that we are unable to set readonly attrs through __init__
703         for readonly_key in Task.read_only_fields:
704             kwargs = {'description': 'test task', readonly_key: 'value'}
705             self.assertRaises(RuntimeError,
706                               lambda: Task(self.tw, **kwargs))
707
708     def test_setting_read_only_attrs_through_setitem(self):
709         # Test that we are unable to set readonly attrs through __init__
710         for readonly_key in Task.read_only_fields:
711             t = Task(self.tw, description='test task')
712             self.assertRaises(RuntimeError,
713                               lambda: t.__setitem__(readonly_key, 'value'))
714
715     def test_saving_unmodified_task(self):
716         t = Task(self.tw, description="test task")
717         t.save()
718         t.save()
719
720     def test_adding_tag_by_appending(self):
721         t = Task(self.tw, description="test task", tags=['test1'])
722         t.save()
723         t['tags'].add('test2')
724         t.save()
725         self.assertEqual(t['tags'], set(['test1', 'test2']))
726
727     def test_adding_tag_twice(self):
728         t = Task(self.tw, description="test task", tags=['test1'])
729         t.save()
730         t['tags'].add('test2')
731         t['tags'].add('test2')
732         t.save()
733         self.assertEqual(t['tags'], set(['test1', 'test2']))
734
735     def test_adding_tag_by_appending_empty(self):
736         t = Task(self.tw, description="test task")
737         t.save()
738         t['tags'].add('test')
739         t.save()
740         self.assertEqual(t['tags'], set(['test']))
741
742     def test_serializers_returning_empty_string_for_none(self):
743         # Test that any serializer returns '' when passed None
744         t = Task(self.tw)
745         serializers = [getattr(t, serializer_name) for serializer_name in
746                        filter(lambda x: x.startswith('serialize_'), dir(t))]
747         for serializer in serializers:
748             self.assertEqual(serializer(None), '')
749
750     def test_deserializer_returning_empty_value_for_empty_string(self):
751         # Test that any deserializer returns empty value when passed ''
752         t = Task(self.tw)
753         deserializers = [getattr(t, deserializer_name) for deserializer_name in
754                         filter(lambda x: x.startswith('deserialize_'), dir(t))]
755         for deserializer in deserializers:
756             self.assertTrue(deserializer('') in (None, [], set()))
757
758     def test_normalizers_handling_none(self):
759         # Test that any normalizer can handle None as a valid value
760         t = Task(self.tw)
761
762         for key in TASK_STANDARD_ATTRS:
763             t._normalize(key, None)
764
765     def test_recurrent_task_generation(self):
766         today = datetime.date.today()
767         t = Task(self.tw, description="brush teeth",
768                  due=today, recur="daily")
769         t.save()
770         self.assertEqual(len(self.tw.tasks.pending()), 2)
771
772     def test_spawned_task_parent(self):
773         today = datetime.date.today()
774         t = Task(self.tw, description="brush teeth",
775                  due=today, recur="daily")
776         t.save()
777
778         spawned = self.tw.tasks.pending().get(due=today)
779         assert spawned['parent'] == t
780
781     def test_modify_number_of_tasks_at_once(self):
782         for i in range(1, 100):
783             Task(self.tw, description="test task %d" % i, tags=['test']).save()
784
785         self.tw.execute_command(['+test', 'mod', 'unified', 'description'])
786
787     def test_return_all_from_executed_command(self):
788         Task(self.tw, description="test task", tags=['test']).save()
789         out, err, rc = self.tw.execute_command(['count'], return_all=True)
790         self.assertEqual(rc, 0)
791
792     def test_return_all_from_failed_executed_command(self):
793         Task(self.tw, description="test task", tags=['test']).save()
794         out, err, rc = self.tw.execute_command(['countinvalid'],
795             return_all=True, allow_failure=False)
796         self.assertNotEqual(rc, 0)
797
798
799 class TaskFromHookTest(TasklibTest):
800
801     input_add_data = six.StringIO(
802         '{"description":"Buy some milk",'
803         '"entry":"20141118T050231Z",'
804         '"status":"pending",'
805         '"start":"20141119T152233Z",'
806         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
807
808     input_add_data_recurring = six.StringIO(
809         '{"description":"Mow the lawn",'
810         '"entry":"20160210T224304Z",'
811         '"parent":"62da6227-519c-42c2-915d-dccada926ad7",'
812         '"recur":"weekly",'
813         '"status":"pending",'
814         '"uuid":"81305335-0237-49ff-8e87-b3cdc2369cec"}')
815
816     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
817         '{"description":"Buy some milk finally",'
818         '"entry":"20141118T050231Z",'
819         '"status":"completed",'
820         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
821
822     exported_raw_data = (
823         '{"project":"Home",'
824          '"due":"20150101T232323Z",'
825          '"description":"test task"}')
826
827     def test_setting_up_from_add_hook_input(self):
828         t = Task.from_input(input_file=self.input_add_data, backend=self.tw)
829         self.assertEqual(t['description'], "Buy some milk")
830         self.assertEqual(t.pending, True)
831
832     def test_setting_up_from_add_hook_input_recurring(self):
833         t = Task.from_input(input_file=self.input_add_data_recurring,
834                             backend=self.tw)
835         self.assertEqual(t['description'], "Mow the lawn")
836         self.assertEqual(t.pending, True)
837
838     def test_setting_up_from_modified_hook_input(self):
839         t = Task.from_input(input_file=self.input_modify_data, modify=True,
840                             backend=self.tw)
841         self.assertEqual(t['description'], "Buy some milk finally")
842         self.assertEqual(t.pending, False)
843         self.assertEqual(t.completed, True)
844
845         self.assertEqual(t._original_data['status'], "pending")
846         self.assertEqual(t._original_data['description'], "Buy some milk")
847         self.assertEqual(set(t._modified_fields),
848                          set(['status', 'description', 'start']))
849
850     def test_export_data(self):
851         t = Task(self.tw, description="test task",
852             project="Home",
853             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
854
855         # Check that the output is a permutation of:
856         # {"project":"Home","description":"test task","due":"20150101232323Z"}
857         allowed_segments = self.exported_raw_data[1:-1].split(',')
858         allowed_output = [
859             '{' + ','.join(segments) + '}'
860             for segments in itertools.permutations(allowed_segments)
861         ]
862
863         self.assertTrue(any(t.export_data() == expected
864                             for expected in allowed_output))
865
866 class TimezoneAwareDatetimeTest(TasklibTest):
867
868     def setUp(self):
869         super(TimezoneAwareDatetimeTest, self).setUp()
870         self.zone = local_zone
871         self.localdate_naive = datetime.datetime(2015,2,2)
872         self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
873         self.localtime_aware = self.zone.localize(self.localtime_naive)
874         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
875
876     def test_timezone_naive_datetime_setitem(self):
877         t = Task(self.tw, description="test task")
878         t['due'] = self.localtime_naive
879         self.assertEqual(t['due'], self.localtime_aware)
880
881     def test_timezone_naive_datetime_using_init(self):
882         t = Task(self.tw, description="test task", due=self.localtime_naive)
883         self.assertEqual(t['due'], self.localtime_aware)
884
885     def test_filter_by_naive_datetime(self):
886         t = Task(self.tw, description="task1", due=self.localtime_naive)
887         t.save()
888         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
889         self.assertEqual(len(matching_tasks), 1)
890
891     def test_serialize_naive_datetime(self):
892         t = Task(self.tw, description="task1", due=self.localtime_naive)
893         self.assertEqual(json.loads(t.export_data())['due'],
894                          self.utctime_aware.strftime(DATE_FORMAT))
895
896     def test_timezone_naive_date_setitem(self):
897         t = Task(self.tw, description="test task")
898         t['due'] = self.localdate_naive
899         self.assertEqual(t['due'], self.localtime_aware)
900
901     def test_timezone_naive_date_using_init(self):
902         t = Task(self.tw, description="test task", due=self.localdate_naive)
903         self.assertEqual(t['due'], self.localtime_aware)
904
905     def test_filter_by_naive_date(self):
906         t = Task(self.tw, description="task1", due=self.localdate_naive)
907         t.save()
908         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
909         self.assertEqual(len(matching_tasks), 1)
910
911     def test_serialize_naive_date(self):
912         t = Task(self.tw, description="task1", due=self.localdate_naive)
913         self.assertEqual(json.loads(t.export_data())['due'],
914                          self.utctime_aware.strftime(DATE_FORMAT))
915
916     def test_timezone_aware_datetime_setitem(self):
917         t = Task(self.tw, description="test task")
918         t['due'] = self.localtime_aware
919         self.assertEqual(t['due'], self.localtime_aware)
920
921     def test_timezone_aware_datetime_using_init(self):
922         t = Task(self.tw, description="test task", due=self.localtime_aware)
923         self.assertEqual(t['due'], self.localtime_aware)
924
925     def test_filter_by_aware_datetime(self):
926         t = Task(self.tw, description="task1", due=self.localtime_aware)
927         t.save()
928         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
929         self.assertEqual(len(matching_tasks), 1)
930
931     def test_serialize_aware_datetime(self):
932         t = Task(self.tw, description="task1", due=self.localtime_aware)
933         self.assertEqual(json.loads(t.export_data())['due'],
934                          self.utctime_aware.strftime(DATE_FORMAT))
935
936 class DatetimeStringTest(TasklibTest):
937
938     def test_simple_now_conversion(self):
939         if self.tw.version < six.text_type('2.4.0'):
940             # Python2.6 does not support SkipTest. As a workaround
941             # mark the test as passed by exiting.
942             if getattr(unittest, 'SkipTest', None) is not None:
943                 raise unittest.SkipTest()
944             else:
945                 return
946
947         t = Task(self.tw, description="test task", due="now")
948         now = local_zone.localize(datetime.datetime.now())
949
950         # Assert that both times are not more than 5 seconds apart
951         if sys.version_info < (2,7):
952             self.assertTrue(total_seconds_2_6(now - t['due']) < 5)
953             self.assertTrue(total_seconds_2_6(t['due'] - now) < 5)
954         else:
955             self.assertTrue((now - t['due']).total_seconds() < 5)
956             self.assertTrue((t['due'] - now).total_seconds() < 5)
957
958     def test_simple_eoy_conversion(self):
959         if self.tw.version < six.text_type('2.4.0'):
960             # Python2.6 does not support SkipTest. As a workaround
961             # mark the test as passed by exiting.
962             if getattr(unittest, 'SkipTest', None) is not None:
963                 raise unittest.SkipTest()
964             else:
965                 return
966
967         t = Task(self.tw, description="test task", due="eoy")
968         now = local_zone.localize(datetime.datetime.now())
969         eoy = local_zone.localize(datetime.datetime(
970             year=now.year,
971             month=12,
972             day=31,
973             hour=23,
974             minute=59,
975             second=59
976             ))
977         self.assertEqual(eoy, t['due'])
978
979     def test_complex_eoy_conversion(self):
980         if self.tw.version < six.text_type('2.4.0'):
981             # Python2.6 does not support SkipTest. As a workaround
982             # mark the test as passed by exiting.
983             if getattr(unittest, 'SkipTest', None) is not None:
984                 raise unittest.SkipTest()
985             else:
986                 return
987
988         t = Task(self.tw, description="test task", due="eoy - 4 months")
989         now = local_zone.localize(datetime.datetime.now())
990         due_date = local_zone.localize(datetime.datetime(
991             year=now.year,
992             month=12,
993             day=31,
994             hour=23,
995             minute=59,
996             second=59
997             )) - datetime.timedelta(0,4 * 30 * 86400)
998         self.assertEqual(due_date, t['due'])
999
1000     def test_filtering_with_string_datetime(self):
1001         if self.tw.version < six.text_type('2.4.0'):
1002             # Python2.6 does not support SkipTest. As a workaround
1003             # mark the test as passed by exiting.
1004             if getattr(unittest, 'SkipTest', None) is not None:
1005                 raise unittest.SkipTest()
1006             else:
1007                 return
1008
1009         t = Task(self.tw, description="test task",
1010                  due=datetime.datetime.now() - datetime.timedelta(0,2))
1011         t.save()
1012         self.assertEqual(len(self.tw.tasks.filter(due__before="now")), 1)
1013
1014 class AnnotationTest(TasklibTest):
1015
1016     def setUp(self):
1017         super(AnnotationTest, self).setUp()
1018         Task(self.tw, description="test task").save()
1019
1020     def test_adding_annotation(self):
1021         task = self.tw.tasks.get()
1022         task.add_annotation('test annotation')
1023         self.assertEqual(len(task['annotations']), 1)
1024         ann = task['annotations'][0]
1025         self.assertEqual(ann['description'], 'test annotation')
1026
1027     def test_removing_annotation(self):
1028         task = self.tw.tasks.get()
1029         task.add_annotation('test annotation')
1030         ann = task['annotations'][0]
1031         ann.remove()
1032         self.assertEqual(len(task['annotations']), 0)
1033
1034     def test_removing_annotation_by_description(self):
1035         task = self.tw.tasks.get()
1036         task.add_annotation('test annotation')
1037         task.remove_annotation('test annotation')
1038         self.assertEqual(len(task['annotations']), 0)
1039
1040     def test_removing_annotation_by_obj(self):
1041         task = self.tw.tasks.get()
1042         task.add_annotation('test annotation')
1043         ann = task['annotations'][0]
1044         task.remove_annotation(ann)
1045         self.assertEqual(len(task['annotations']), 0)
1046
1047     def test_annotation_after_modification(self):
1048          task = self.tw.tasks.get()
1049          task['project'] = 'test'
1050          task.add_annotation('I should really do this task')
1051          self.assertEqual(task['project'], 'test')
1052          task.save()
1053          self.assertEqual(task['project'], 'test')
1054
1055     def test_serialize_annotations(self):
1056         # Test that serializing annotations is possible
1057         t = Task(self.tw, description="test")
1058         t.save()
1059
1060         t.add_annotation("annotation1")
1061         t.add_annotation("annotation2")
1062
1063         data = t._serialize('annotations', t._data['annotations'])
1064
1065         self.assertEqual(len(data), 2)
1066         self.assertEqual(type(data[0]), dict)
1067         self.assertEqual(type(data[1]), dict)
1068
1069         self.assertEqual(data[0]['description'], "annotation1")
1070         self.assertEqual(data[1]['description'], "annotation2")
1071
1072
1073 class UnicodeTest(TasklibTest):
1074
1075     def test_unicode_task(self):
1076         Task(self.tw, description=six.u("†åßk")).save()
1077         self.tw.tasks.get()
1078
1079     def test_filter_by_unicode_task(self):
1080         Task(self.tw, description=six.u("†åßk")).save()
1081         tasks = self.tw.tasks.filter(description=six.u("†åßk"))
1082         self.assertEqual(len(tasks), 1)
1083
1084     def test_non_unicode_task(self):
1085         Task(self.tw, description="test task").save()
1086         self.tw.tasks.get()
1087
1088 class ReadOnlyDictViewTest(unittest.TestCase):
1089
1090     def setUp(self):
1091         self.sample = dict(l=[1,2,3], d={'k':'v'})
1092         self.original_sample = copy.deepcopy(self.sample)
1093         self.view = ReadOnlyDictView(self.sample)
1094
1095     def test_readonlydictview_getitem(self):
1096         l = self.view['l']
1097         self.assertEqual(l, self.sample['l'])
1098
1099         # Assert that modification changed only copied value
1100         l.append(4)
1101         self.assertNotEqual(l, self.sample['l'])
1102
1103         # Assert that viewed dict is not changed
1104         self.assertEqual(self.sample, self.original_sample)
1105
1106     def test_readonlydictview_contains(self):
1107         self.assertEqual('l' in self.view, 'l' in self.sample)
1108         self.assertEqual('d' in self.view, 'd' in self.sample)
1109         self.assertEqual('k' in self.view, 'k' in self.sample)
1110
1111         # Assert that viewed dict is not changed
1112         self.assertEqual(self.sample, self.original_sample)
1113
1114     def test_readonlydictview_iter(self):
1115         self.assertEqual(list(k for k in self.view),
1116                          list(k for k in self.sample))
1117
1118         # Assert the view is correct after modification
1119         self.sample['new'] = 'value'
1120         self.assertEqual(list(k for k in self.view),
1121                          list(k for k in self.sample))
1122
1123     def test_readonlydictview_len(self):
1124         self.assertEqual(len(self.view), len(self.sample))
1125
1126         # Assert the view is correct after modification
1127         self.sample['new'] = 'value'
1128         self.assertEqual(len(self.view), len(self.sample))
1129
1130     def test_readonlydictview_get(self):
1131         l = self.view.get('l')
1132         self.assertEqual(l, self.sample.get('l'))
1133
1134         # Assert that modification changed only copied value
1135         l.append(4)
1136         self.assertNotEqual(l, self.sample.get('l'))
1137
1138         # Assert that viewed dict is not changed
1139         self.assertEqual(self.sample, self.original_sample)
1140
1141     def test_readonlydict_items(self):
1142         view_items = self.view.items()
1143         sample_items = list(self.sample.items())
1144         self.assertEqual(view_items, sample_items)
1145
1146         view_items.append('newkey')
1147         self.assertNotEqual(view_items, sample_items)
1148         self.assertEqual(self.sample, self.original_sample)
1149
1150     def test_readonlydict_values(self):
1151         view_values = self.view.values()
1152         sample_values = list(self.sample.values())
1153         self.assertEqual(view_values, sample_values)
1154
1155         view_list_item = list(filter(lambda x: type(x) is list,
1156                                      view_values))[0]
1157         view_list_item.append(4)
1158         self.assertNotEqual(view_values, sample_values)
1159         self.assertEqual(self.sample, self.original_sample)
1160
1161
1162 class LazyUUIDTaskTest(TasklibTest):
1163
1164     def setUp(self):
1165         super(LazyUUIDTaskTest, self).setUp()
1166
1167         self.stored = Task(self.tw, description="this is test task")
1168         self.stored.save()
1169
1170         self.lazy = LazyUUIDTask(self.tw, self.stored['uuid'])
1171
1172     def test_uuid_non_conversion(self):
1173         assert self.stored['uuid'] == self.lazy['uuid']
1174         assert type(self.lazy) is LazyUUIDTask
1175
1176     def test_lazy_explicit_conversion(self):
1177         assert type(self.lazy) is LazyUUIDTask
1178         self.lazy.replace()
1179         assert type(self.lazy) is Task
1180
1181     def test_conversion_key(self):
1182         assert self.stored['description'] == self.lazy['description']
1183         assert type(self.lazy) is Task
1184
1185     def test_conversion_attribute(self):
1186         assert type(self.lazy) is LazyUUIDTask
1187         assert self.lazy.completed is False
1188         assert type(self.lazy) is Task
1189
1190     def test_normal_to_lazy_equality(self):
1191         assert self.stored == self.lazy
1192         assert not self.stored != self.lazy
1193         assert type(self.lazy) is LazyUUIDTask
1194
1195     def test_lazy_to_lazy_equality(self):
1196         lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
1197         lazy2 = LazyUUIDTask(self.tw, self.stored['uuid'])
1198
1199         assert lazy1 == lazy2
1200         assert not lazy1 != lazy2
1201         assert type(lazy1) is LazyUUIDTask
1202         assert type(lazy2) is LazyUUIDTask
1203
1204     def test_normal_to_lazy_inequality(self):
1205         # Create a different UUID by changing the last letter
1206         wrong_uuid = self.stored['uuid']
1207         wrong_uuid = wrong_uuid[:-1] + ('a' if wrong_uuid[-1] != 'a' else 'b')
1208
1209         wrong_lazy = LazyUUIDTask(self.tw, wrong_uuid)
1210
1211         assert not self.stored == wrong_lazy
1212         assert self.stored != wrong_lazy
1213         assert type(wrong_lazy) is LazyUUIDTask
1214
1215     def test_lazy_to_lazy_inequality(self):
1216         # Create a different UUID by changing the last letter
1217         wrong_uuid = self.stored['uuid']
1218         wrong_uuid = wrong_uuid[:-1] + ('a' if wrong_uuid[-1] != 'a' else 'b')
1219
1220         lazy1 = LazyUUIDTask(self.tw, self.stored['uuid'])
1221         lazy2 = LazyUUIDTask(self.tw, wrong_uuid)
1222
1223         assert not lazy1 == lazy2
1224         assert lazy1 != lazy2
1225         assert type(lazy1) is LazyUUIDTask
1226         assert type(lazy2) is LazyUUIDTask
1227
1228     def test_lazy_in_queryset(self):
1229         tasks = self.tw.tasks.filter(uuid=self.stored['uuid'])
1230
1231         assert self.lazy in tasks
1232         assert type(self.lazy) is LazyUUIDTask
1233
1234     def test_lazy_saved(self):
1235         assert self.lazy.saved is True
1236
1237     def test_lazy_modified(self):
1238         assert self.lazy.modified is False
1239
1240     def test_lazy_modified_fields(self):
1241         assert self.lazy._modified_fields == set()
1242
1243
1244 class LazyUUIDTaskSetTest(TasklibTest):
1245
1246     def setUp(self):
1247         super(LazyUUIDTaskSetTest, self).setUp()
1248
1249         self.task1 = Task(self.tw, description="task 1")
1250         self.task2 = Task(self.tw, description="task 2")
1251         self.task3 = Task(self.tw, description="task 3")
1252
1253         self.task1.save()
1254         self.task2.save()
1255         self.task3.save()
1256
1257         self.uuids = (
1258             self.task1['uuid'],
1259             self.task2['uuid'],
1260             self.task3['uuid']
1261         )
1262
1263         self.lazy = LazyUUIDTaskSet(self.tw, self.uuids)
1264
1265     def test_length(self):
1266         assert len(self.lazy) == 3
1267         assert type(self.lazy) is LazyUUIDTaskSet
1268
1269     def test_contains(self):
1270         assert self.task1 in self.lazy
1271         assert self.task2 in self.lazy
1272         assert self.task3 in self.lazy
1273         assert type(self.lazy) is LazyUUIDTaskSet
1274
1275     def test_eq_lazy(self):
1276         new_lazy = LazyUUIDTaskSet(self.tw, self.uuids)
1277         assert self.lazy == new_lazy
1278         assert not self.lazy != new_lazy
1279         assert type(self.lazy) is LazyUUIDTaskSet
1280
1281     def test_eq_real(self):
1282         assert self.lazy == self.tw.tasks.all()
1283         assert self.tw.tasks.all() == self.lazy
1284         assert not self.lazy != self.tw.tasks.all()
1285
1286         assert type(self.lazy) is LazyUUIDTaskSet
1287
1288     def test_union(self):
1289         taskset = set([self.task1])
1290         lazyset = LazyUUIDTaskSet(
1291             self.tw,
1292             (self.task2['uuid'], self.task3['uuid'])
1293         )
1294
1295         assert taskset | lazyset == self.lazy
1296         assert lazyset | taskset == self.lazy
1297         assert taskset.union(lazyset) == self.lazy
1298         assert lazyset.union(taskset) == self.lazy
1299
1300         lazyset |= taskset
1301         assert lazyset == self.lazy
1302
1303     def test_difference(self):
1304         taskset = set([self.task1, self.task2])
1305         lazyset = LazyUUIDTaskSet(
1306             self.tw,
1307             (self.task2['uuid'], self.task3['uuid'])
1308         )
1309
1310         assert taskset - lazyset == set([self.task1])
1311         assert lazyset - taskset == set([self.task3])
1312         assert taskset.difference(lazyset) == set([self.task1])
1313         assert lazyset.difference(taskset) == set([self.task3])
1314
1315         lazyset -= taskset
1316         assert lazyset == set([self.task3])
1317
1318     def test_symmetric_difference(self):
1319         taskset = set([self.task1, self.task2])
1320         lazyset = LazyUUIDTaskSet(
1321             self.tw,
1322             (self.task2['uuid'], self.task3['uuid'])
1323         )
1324
1325         assert taskset ^ lazyset == set([self.task1, self.task3])
1326         assert lazyset ^ taskset == set([self.task1, self.task3])
1327         assert taskset.symmetric_difference(lazyset) == set([self.task1, self.task3])
1328         assert lazyset.symmetric_difference(taskset) == set([self.task1, self.task3])
1329
1330         lazyset ^= taskset
1331         assert lazyset == set([self.task1, self.task3])
1332
1333     def test_intersection(self):
1334         taskset = set([self.task1, self.task2])
1335         lazyset = LazyUUIDTaskSet(
1336             self.tw,
1337             (self.task2['uuid'], self.task3['uuid'])
1338         )
1339
1340         assert taskset & lazyset == set([self.task2])
1341         assert lazyset & taskset == set([self.task2])
1342         assert taskset.intersection(lazyset) == set([self.task2])
1343         assert lazyset.intersection(taskset) == set([self.task2])
1344
1345         lazyset &= taskset
1346         assert lazyset == set([self.task2])
1347
1348
1349 class TaskWarriorBackendTest(TasklibTest):
1350
1351     def test_config(self):
1352         assert self.tw.config['nag'] == "You have more urgent tasks."
1353         assert self.tw.config['debug'] == "no"