]> 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:

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