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

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