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