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

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