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

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