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

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