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

SerializingObject: Restrict usage of 'task calc' conversion for those TW versions...
[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 tempfile
11 import unittest
12
13 from .task import TaskWarrior, Task, ReadOnlyDictView, local_zone, DATE_FORMAT
14
15 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
16 TASK_STANDARD_ATTRS = (
17     'status',
18     'uuid',
19     'entry',
20     'description',
21     'start',
22     'end',
23     'due',
24     'until',
25     'wait',
26     'modified',
27     'scheduled',
28     'recur',
29     'mask',
30     'imask',
31     'parent',
32     'project',
33     'priority',
34     'depends',
35     'tags',
36     'annotations',
37 )
38
39 class TasklibTest(unittest.TestCase):
40
41     def setUp(self):
42         self.tmp = tempfile.mkdtemp(dir='.')
43         self.tw = TaskWarrior(data_location=self.tmp, taskrc_location='/')
44
45     def tearDown(self):
46         shutil.rmtree(self.tmp)
47
48
49 class TaskFilterTest(TasklibTest):
50
51     def test_all_empty(self):
52         self.assertEqual(len(self.tw.tasks.all()), 0)
53
54     def test_all_non_empty(self):
55         Task(self.tw, description="test task").save()
56         self.assertEqual(len(self.tw.tasks.all()), 1)
57         self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
58         self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
59
60     def test_pending_non_empty(self):
61         Task(self.tw, description="test task").save()
62         self.assertEqual(len(self.tw.tasks.pending()), 1)
63         self.assertEqual(self.tw.tasks.pending()[0]['description'],
64                          'test task')
65         self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
66
67     def test_completed_empty(self):
68         Task(self.tw, description="test task").save()
69         self.assertEqual(len(self.tw.tasks.completed()), 0)
70
71     def test_completed_non_empty(self):
72         Task(self.tw, description="test task").save()
73         self.assertEqual(len(self.tw.tasks.completed()), 0)
74         self.tw.tasks.all()[0].done()
75         self.assertEqual(len(self.tw.tasks.completed()), 1)
76
77     def test_filtering_by_attribute(self):
78         Task(self.tw, description="no priority task").save()
79         Task(self.tw, priority="H", description="high priority task").save()
80         self.assertEqual(len(self.tw.tasks.all()), 2)
81
82         # Assert that the correct number of tasks is returned
83         self.assertEqual(len(self.tw.tasks.filter(priority="H")), 1)
84
85         # Assert that the correct tasks are returned
86         high_priority_task = self.tw.tasks.get(priority="H")
87         self.assertEqual(high_priority_task['description'], "high priority task")
88
89     def test_filtering_by_empty_attribute(self):
90         Task(self.tw, description="no priority task").save()
91         Task(self.tw, priority="H", description="high priority task").save()
92         self.assertEqual(len(self.tw.tasks.all()), 2)
93
94         # Assert that the correct number of tasks is returned
95         self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
96
97         # Assert that the correct tasks are returned
98         no_priority_task = self.tw.tasks.get(priority=None)
99         self.assertEqual(no_priority_task['description'], "no priority task")
100
101     def test_filter_for_task_with_space_in_descripition(self):
102         task = Task(self.tw, description="test task")
103         task.save()
104
105         filtered_task = self.tw.tasks.get(description="test task")
106         self.assertEqual(filtered_task['description'], "test task")
107
108     def test_filter_for_task_without_space_in_descripition(self):
109         task = Task(self.tw, description="test")
110         task.save()
111
112         filtered_task = self.tw.tasks.get(description="test")
113         self.assertEqual(filtered_task['description'], "test")
114
115     def test_filter_for_task_with_space_in_project(self):
116         task = Task(self.tw, description="test", project="random project")
117         task.save()
118
119         filtered_task = self.tw.tasks.get(project="random project")
120         self.assertEqual(filtered_task['project'], "random project")
121
122     def test_filter_for_task_without_space_in_project(self):
123         task = Task(self.tw, description="test", project="random")
124         task.save()
125
126         filtered_task = self.tw.tasks.get(project="random")
127         self.assertEqual(filtered_task['project'], "random")
128
129     def test_filter_with_empty_uuid(self):
130         self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid=''))
131
132     def test_filter_dummy_by_status(self):
133         t = Task(self.tw, description="test")
134         t.save()
135
136         tasks = self.tw.tasks.filter(status=t['status'])
137         self.assertEqual(list(tasks), [t])
138
139     def test_filter_dummy_by_uuid(self):
140         t = Task(self.tw, description="test")
141         t.save()
142
143         tasks = self.tw.tasks.filter(uuid=t['uuid'])
144         self.assertEqual(list(tasks), [t])
145
146     def test_filter_dummy_by_entry(self):
147         t = Task(self.tw, description="test")
148         t.save()
149
150         tasks = self.tw.tasks.filter(entry=t['entry'])
151         self.assertEqual(list(tasks), [t])
152
153     def test_filter_dummy_by_description(self):
154         t = Task(self.tw, description="test")
155         t.save()
156
157         tasks = self.tw.tasks.filter(description=t['description'])
158         self.assertEqual(list(tasks), [t])
159
160     def test_filter_dummy_by_start(self):
161         t = Task(self.tw, description="test")
162         t.save()
163         t.start()
164
165         tasks = self.tw.tasks.filter(start=t['start'])
166         self.assertEqual(list(tasks), [t])
167
168     def test_filter_dummy_by_end(self):
169         t = Task(self.tw, description="test")
170         t.save()
171         t.done()
172
173         tasks = self.tw.tasks.filter(end=t['end'])
174         self.assertEqual(list(tasks), [t])
175
176     def test_filter_dummy_by_due(self):
177         t = Task(self.tw, description="test", due=datetime.datetime.now())
178         t.save()
179
180         tasks = self.tw.tasks.filter(due=t['due'])
181         self.assertEqual(list(tasks), [t])
182
183     def test_filter_dummy_by_until(self):
184         t = Task(self.tw, description="test")
185         t.save()
186
187         tasks = self.tw.tasks.filter(until=t['until'])
188         self.assertEqual(list(tasks), [t])
189
190     def test_filter_dummy_by_modified(self):
191         # Older TW version does not support bumping modified
192         # on save
193         if self.tw.version < six.text_type('2.2.0'):
194             # Python2.6 does not support SkipTest. As a workaround
195             # mark the test as passed by exiting.
196             if getattr(unittest, 'SkipTest', None) is not None:
197                 raise unittest.SkipTest()
198             else:
199                 return
200
201         t = Task(self.tw, description="test")
202         t.save()
203
204         tasks = self.tw.tasks.filter(modified=t['modified'])
205         self.assertEqual(list(tasks), [t])
206
207     def test_filter_dummy_by_scheduled(self):
208         t = Task(self.tw, description="test")
209         t.save()
210
211         tasks = self.tw.tasks.filter(scheduled=t['scheduled'])
212         self.assertEqual(list(tasks), [t])
213
214     def test_filter_dummy_by_tags(self):
215         t = Task(self.tw, description="test", tags=["home"])
216         t.save()
217
218         tasks = self.tw.tasks.filter(tags=t['tags'])
219         self.assertEqual(list(tasks), [t])
220
221     def test_filter_dummy_by_projects(self):
222         t = Task(self.tw, description="test", project="random")
223         t.save()
224
225         tasks = self.tw.tasks.filter(project=t['project'])
226         self.assertEqual(list(tasks), [t])
227
228     def test_filter_by_priority(self):
229         t = Task(self.tw, description="test", priority="H")
230         t.save()
231
232         tasks = self.tw.tasks.filter(priority=t['priority'])
233         self.assertEqual(list(tasks), [t])
234
235
236 class TaskTest(TasklibTest):
237
238     def test_create_unsaved_task(self):
239         # Make sure a new task is not saved unless explicitly called for
240         t = Task(self.tw, description="test task")
241         self.assertEqual(len(self.tw.tasks.all()), 0)
242
243     # TODO: once python 2.6 compatiblity is over, use context managers here
244     #       and in all subsequent tests for assertRaises
245
246     def test_delete_unsaved_task(self):
247         t = Task(self.tw, description="test task")
248         self.assertRaises(Task.NotSaved, t.delete)
249
250     def test_complete_unsaved_task(self):
251         t = Task(self.tw, description="test task")
252         self.assertRaises(Task.NotSaved, t.done)
253
254     def test_refresh_unsaved_task(self):
255         t = Task(self.tw, description="test task")
256         self.assertRaises(Task.NotSaved, t.refresh)
257
258     def test_start_unsaved_task(self):
259         t = Task(self.tw, description="test task")
260         self.assertRaises(Task.NotSaved, t.start)
261
262     def test_delete_deleted_task(self):
263         t = Task(self.tw, description="test task")
264         t.save()
265         t.delete()
266
267         self.assertRaises(Task.DeletedTask, t.delete)
268
269     def test_complete_completed_task(self):
270         t = Task(self.tw, description="test task")
271         t.save()
272         t.done()
273
274         self.assertRaises(Task.CompletedTask, t.done)
275
276     def test_start_completed_task(self):
277         t = Task(self.tw, description="test task")
278         t.save()
279         t.done()
280
281         self.assertRaises(Task.CompletedTask, t.start)
282
283     def test_complete_deleted_task(self):
284         t = Task(self.tw, description="test task")
285         t.save()
286         t.delete()
287
288         self.assertRaises(Task.DeletedTask, t.done)
289
290     def test_start_completed_task(self):
291         t = Task(self.tw, description="test task")
292         t.save()
293         t.done()
294
295         self.assertRaises(Task.CompletedTask, t.start)
296
297     def test_starting_task(self):
298         t = Task(self.tw, description="test task")
299         now = t.datetime_normalizer(datetime.datetime.now())
300         t.save()
301         t.start()
302
303         self.assertTrue(now.replace(microsecond=0) <= t['start'])
304         self.assertEqual(t['status'], 'pending')
305
306     def test_completing_task(self):
307         t = Task(self.tw, description="test task")
308         now = t.datetime_normalizer(datetime.datetime.now())
309         t.save()
310         t.done()
311
312         self.assertTrue(now.replace(microsecond=0) <= t['end'])
313         self.assertEqual(t['status'], 'completed')
314
315     def test_deleting_task(self):
316         t = Task(self.tw, description="test task")
317         now = t.datetime_normalizer(datetime.datetime.now())
318         t.save()
319         t.delete()
320
321         self.assertTrue(now.replace(microsecond=0) <= t['end'])
322         self.assertEqual(t['status'], 'deleted')
323
324     def test_modify_simple_attribute_without_space(self):
325         t = Task(self.tw, description="test")
326         t.save()
327
328         self.assertEquals(t['description'], "test")
329
330         t['description'] = "test-modified"
331         t.save()
332
333         self.assertEquals(t['description'], "test-modified")
334
335     def test_modify_simple_attribute_with_space(self):
336         # Space can pose problems with parsing
337         t = Task(self.tw, description="test task")
338         t.save()
339
340         self.assertEquals(t['description'], "test task")
341
342         t['description'] = "test task modified"
343         t.save()
344
345         self.assertEquals(t['description'], "test task modified")
346
347     def test_empty_dependency_set_of_unsaved_task(self):
348         t = Task(self.tw, description="test task")
349         self.assertEqual(t['depends'], set())
350
351     def test_empty_dependency_set_of_saved_task(self):
352         t = Task(self.tw, description="test task")
353         t.save()
354         self.assertEqual(t['depends'], set())
355
356     def test_set_unsaved_task_as_dependency(self):
357         # Adds only one dependency to task with no dependencies
358         t = Task(self.tw, description="test task")
359         dependency = Task(self.tw, description="needs to be done first")
360
361         # We only save the parent task, dependency task is unsaved
362         t.save()
363         t['depends'] = set([dependency])
364
365         self.assertRaises(Task.NotSaved, t.save)
366
367     def test_set_simple_dependency_set(self):
368         # Adds only one dependency to task with no dependencies
369         t = Task(self.tw, description="test task")
370         dependency = Task(self.tw, description="needs to be done first")
371
372         t.save()
373         dependency.save()
374
375         t['depends'] = set([dependency])
376
377         self.assertEqual(t['depends'], set([dependency]))
378
379     def test_set_complex_dependency_set(self):
380         # Adds two dependencies to task with no dependencies
381         t = Task(self.tw, description="test task")
382         dependency1 = Task(self.tw, description="needs to be done first")
383         dependency2 = Task(self.tw, description="needs to be done second")
384
385         t.save()
386         dependency1.save()
387         dependency2.save()
388
389         t['depends'] = set([dependency1, dependency2])
390
391         self.assertEqual(t['depends'], set([dependency1, dependency2]))
392
393     def test_remove_from_dependency_set(self):
394         # Removes dependency from task with two dependencies
395         t = Task(self.tw, description="test task")
396         dependency1 = Task(self.tw, description="needs to be done first")
397         dependency2 = Task(self.tw, description="needs to be done second")
398
399         dependency1.save()
400         dependency2.save()
401
402         t['depends'] = set([dependency1, dependency2])
403         t.save()
404
405         t['depends'].remove(dependency2)
406         t.save()
407
408         self.assertEqual(t['depends'], set([dependency1]))
409
410     def test_add_to_dependency_set(self):
411         # Adds dependency to task with one dependencies
412         t = Task(self.tw, description="test task")
413         dependency1 = Task(self.tw, description="needs to be done first")
414         dependency2 = Task(self.tw, description="needs to be done second")
415
416         dependency1.save()
417         dependency2.save()
418
419         t['depends'] = set([dependency1])
420         t.save()
421
422         t['depends'].add(dependency2)
423         t.save()
424
425         self.assertEqual(t['depends'], set([dependency1, dependency2]))
426
427     def test_add_to_empty_dependency_set(self):
428         # Adds dependency to task with one dependencies
429         t = Task(self.tw, description="test task")
430         dependency = Task(self.tw, description="needs to be done first")
431
432         dependency.save()
433
434         t['depends'].add(dependency)
435         t.save()
436
437         self.assertEqual(t['depends'], set([dependency]))
438
439     def test_simple_dependency_set_save_repeatedly(self):
440         # Adds only one dependency to task with no dependencies
441         t = Task(self.tw, description="test task")
442         dependency = Task(self.tw, description="needs to be done first")
443         dependency.save()
444
445         t['depends'] = set([dependency])
446         t.save()
447
448         # We taint the task, but keep depends intact
449         t['description'] = "test task modified"
450         t.save()
451
452         self.assertEqual(t['depends'], set([dependency]))
453
454         # We taint the task, but assign the same set to the depends
455         t['depends'] = set([dependency])
456         t['description'] = "test task modified again"
457         t.save()
458
459         self.assertEqual(t['depends'], set([dependency]))
460
461     def test_compare_different_tasks(self):
462         # Negative: compare two different tasks
463         t1 = Task(self.tw, description="test task")
464         t2 = Task(self.tw, description="test task")
465
466         t1.save()
467         t2.save()
468
469         self.assertEqual(t1 == t2, False)
470
471     def test_compare_same_task_object(self):
472         # Compare Task object wit itself
473         t = Task(self.tw, description="test task")
474         t.save()
475
476         self.assertEqual(t == t, True)
477
478     def test_compare_same_task(self):
479         # Compare the same task using two different objects
480         t1 = Task(self.tw, description="test task")
481         t1.save()
482
483         t2 = self.tw.tasks.get(uuid=t1['uuid'])
484         self.assertEqual(t1 == t2, True)
485
486     def test_compare_unsaved_tasks(self):
487         # t1 and t2 are unsaved tasks, considered to be unequal
488         # despite the content of data
489         t1 = Task(self.tw, description="test task")
490         t2 = Task(self.tw, description="test task")
491
492         self.assertEqual(t1 == t2, False)
493
494     def test_hash_unsaved_tasks(self):
495         # Considered equal, it's the same object
496         t1 = Task(self.tw, description="test task")
497         t2 = t1
498         self.assertEqual(hash(t1) == hash(t2), True)
499
500     def test_hash_same_task(self):
501         # Compare the hash of the task using two different objects
502         t1 = Task(self.tw, description="test task")
503         t1.save()
504
505         t2 = self.tw.tasks.get(uuid=t1['uuid'])
506         self.assertEqual(t1.__hash__(), t2.__hash__())
507
508     def test_adding_task_with_priority(self):
509         t = Task(self.tw, description="test task", priority="M")
510         t.save()
511
512     def test_removing_priority_with_none(self):
513         t = Task(self.tw, description="test task", priority="L")
514         t.save()
515
516         # Remove the priority mark
517         t['priority'] = None
518         t.save()
519
520         # Assert that priority is not there after saving
521         self.assertEqual(t['priority'], None)
522
523     def test_adding_task_with_due_time(self):
524         t = Task(self.tw, description="test task", due=datetime.datetime.now())
525         t.save()
526
527     def test_removing_due_time_with_none(self):
528         t = Task(self.tw, description="test task", due=datetime.datetime.now())
529         t.save()
530
531         # Remove the due timestamp
532         t['due'] = None
533         t.save()
534
535         # Assert that due timestamp is no longer there
536         self.assertEqual(t['due'], None)
537
538     def test_modified_fields_new_task(self):
539         t = Task(self.tw)
540
541         # This should be empty with new task
542         self.assertEqual(set(t._modified_fields), set())
543
544         # Modify the task
545         t['description'] = "test task"
546         self.assertEqual(set(t._modified_fields), set(['description']))
547
548         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
549         self.assertEqual(set(t._modified_fields), set(['description', 'due']))
550
551         t['project'] = "test project"
552         self.assertEqual(set(t._modified_fields),
553                          set(['description', 'due', 'project']))
554
555         # List of modified fields should clear out when saved
556         t.save()
557         self.assertEqual(set(t._modified_fields), set())
558
559         # Reassigning the fields with the same values now should not produce
560         # modified fields
561         t['description'] = "test task"
562         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
563         t['project'] = "test project"
564         self.assertEqual(set(t._modified_fields), set())
565
566     def test_modified_fields_loaded_task(self):
567         t = Task(self.tw)
568
569         # Modify the task
570         t['description'] = "test task"
571         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
572         t['project'] = "test project"
573
574         dependency = Task(self.tw, description="dependency")
575         dependency.save()
576         t['depends'] = set([dependency])
577
578         # List of modified fields should clear out when saved
579         t.save()
580         self.assertEqual(set(t._modified_fields), set())
581
582         # Get the task by using a filter by UUID
583         t2 = self.tw.tasks.get(uuid=t['uuid'])
584
585         # Reassigning the fields with the same values now should not produce
586         # modified fields
587         t['description'] = "test task"
588         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
589         t['project'] = "test project"
590         t['depends'] = set([dependency])
591         self.assertEqual(set(t._modified_fields), set())
592
593     def test_modified_fields_not_affected_by_reading(self):
594         t = Task(self.tw)
595
596         for field in TASK_STANDARD_ATTRS:
597             value = t[field]
598
599         self.assertEqual(set(t._modified_fields), set())
600
601     def test_setting_read_only_attrs_through_init(self):
602         # Test that we are unable to set readonly attrs through __init__
603         for readonly_key in Task.read_only_fields:
604             kwargs = {'description': 'test task', readonly_key: 'value'}
605             self.assertRaises(RuntimeError,
606                               lambda: Task(self.tw, **kwargs))
607
608     def test_setting_read_only_attrs_through_setitem(self):
609         # Test that we are unable to set readonly attrs through __init__
610         for readonly_key in Task.read_only_fields:
611             t = Task(self.tw, description='test task')
612             self.assertRaises(RuntimeError,
613                               lambda: t.__setitem__(readonly_key, 'value'))
614
615     def test_saving_unmodified_task(self):
616         t = Task(self.tw, description="test task")
617         t.save()
618         t.save()
619
620     def test_adding_tag_by_appending(self):
621         t = Task(self.tw, description="test task", tags=['test1'])
622         t.save()
623         t['tags'].append('test2')
624         t.save()
625         self.assertEqual(t['tags'], ['test1', 'test2'])
626
627     def test_adding_tag_by_appending_empty(self):
628         t = Task(self.tw, description="test task")
629         t.save()
630         t['tags'].append('test')
631         t.save()
632         self.assertEqual(t['tags'], ['test'])
633
634     def test_serializers_returning_empty_string_for_none(self):
635         # Test that any serializer returns '' when passed None
636         t = Task(self.tw)
637         serializers = [getattr(t, serializer_name) for serializer_name in
638                        filter(lambda x: x.startswith('serialize_'), dir(t))]
639         for serializer in serializers:
640             self.assertEqual(serializer(None), '')
641
642     def test_deserializer_returning_empty_value_for_empty_string(self):
643         # Test that any deserializer returns empty value when passed ''
644         t = Task(self.tw)
645         deserializers = [getattr(t, deserializer_name) for deserializer_name in
646                         filter(lambda x: x.startswith('deserialize_'), dir(t))]
647         for deserializer in deserializers:
648             self.assertTrue(deserializer('') in (None, [], set()))
649
650     def test_normalizers_handling_none(self):
651         # Test that any normalizer can handle None as a valid value
652         t = Task(self.tw)
653
654         for key in TASK_STANDARD_ATTRS:
655             t._normalize(key, None)
656
657     def test_recurrent_task_generation(self):
658         today = datetime.date.today()
659         t = Task(self.tw, description="brush teeth",
660                  due=today, recur="daily")
661         t.save()
662         self.assertEqual(len(self.tw.tasks.pending()), 2)
663
664 class TaskFromHookTest(TasklibTest):
665
666     input_add_data = six.StringIO(
667         '{"description":"Buy some milk",'
668         '"entry":"20141118T050231Z",'
669         '"status":"pending",'
670         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
671
672     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
673         '{"description":"Buy some milk finally",'
674         '"entry":"20141118T050231Z",'
675         '"status":"completed",'
676         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
677
678     exported_raw_data = (
679         '{"project":"Home",'
680          '"due":"20150101T232323Z",'
681          '"description":"test task"}')
682
683     def test_setting_up_from_add_hook_input(self):
684         t = Task.from_input(input_file=self.input_add_data, warrior=self.tw)
685         self.assertEqual(t['description'], "Buy some milk")
686         self.assertEqual(t.pending, True)
687
688     def test_setting_up_from_modified_hook_input(self):
689         t = Task.from_input(input_file=self.input_modify_data, modify=True,
690                             warrior=self.tw)
691         self.assertEqual(t['description'], "Buy some milk finally")
692         self.assertEqual(t.pending, False)
693         self.assertEqual(t.completed, True)
694
695         self.assertEqual(t._original_data['status'], "pending")
696         self.assertEqual(t._original_data['description'], "Buy some milk")
697         self.assertEqual(set(t._modified_fields),
698                          set(['status', 'description']))
699
700     def test_export_data(self):
701         t = Task(self.tw, description="test task",
702             project="Home",
703             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
704
705         # Check that the output is a permutation of:
706         # {"project":"Home","description":"test task","due":"20150101232323Z"}
707         allowed_segments = self.exported_raw_data[1:-1].split(',')
708         allowed_output = [
709             '{' + ','.join(segments) + '}'
710             for segments in itertools.permutations(allowed_segments)
711         ]
712
713         self.assertTrue(any(t.export_data() == expected
714                             for expected in allowed_output))
715
716 class TimezoneAwareDatetimeTest(TasklibTest):
717
718     def setUp(self):
719         super(TimezoneAwareDatetimeTest, self).setUp()
720         self.zone = local_zone
721         self.localdate_naive = datetime.datetime(2015,2,2)
722         self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
723         self.localtime_aware = self.zone.localize(self.localtime_naive)
724         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
725
726     def test_timezone_naive_datetime_setitem(self):
727         t = Task(self.tw, description="test task")
728         t['due'] = self.localtime_naive
729         self.assertEqual(t['due'], self.localtime_aware)
730
731     def test_timezone_naive_datetime_using_init(self):
732         t = Task(self.tw, description="test task", due=self.localtime_naive)
733         self.assertEqual(t['due'], self.localtime_aware)
734
735     def test_filter_by_naive_datetime(self):
736         t = Task(self.tw, description="task1", due=self.localtime_naive)
737         t.save()
738         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
739         self.assertEqual(len(matching_tasks), 1)
740
741     def test_serialize_naive_datetime(self):
742         t = Task(self.tw, description="task1", due=self.localtime_naive)
743         self.assertEqual(json.loads(t.export_data())['due'], 
744                          self.utctime_aware.strftime(DATE_FORMAT))
745
746     def test_timezone_naive_date_setitem(self):
747         t = Task(self.tw, description="test task")
748         t['due'] = self.localdate_naive
749         self.assertEqual(t['due'], self.localtime_aware)
750
751     def test_timezone_naive_date_using_init(self):
752         t = Task(self.tw, description="test task", due=self.localdate_naive)
753         self.assertEqual(t['due'], self.localtime_aware)
754
755     def test_filter_by_naive_date(self):
756         t = Task(self.tw, description="task1", due=self.localdate_naive)
757         t.save()
758         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
759         self.assertEqual(len(matching_tasks), 1)
760
761     def test_serialize_naive_date(self):
762         t = Task(self.tw, description="task1", due=self.localdate_naive)
763         self.assertEqual(json.loads(t.export_data())['due'], 
764                          self.utctime_aware.strftime(DATE_FORMAT))
765
766     def test_timezone_aware_datetime_setitem(self):
767         t = Task(self.tw, description="test task")
768         t['due'] = self.localtime_aware
769         self.assertEqual(t['due'], self.localtime_aware)
770
771     def test_timezone_aware_datetime_using_init(self):
772         t = Task(self.tw, description="test task", due=self.localtime_aware)
773         self.assertEqual(t['due'], self.localtime_aware)
774
775     def test_filter_by_aware_datetime(self):
776         t = Task(self.tw, description="task1", due=self.localtime_aware)
777         t.save()
778         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
779         self.assertEqual(len(matching_tasks), 1)
780
781     def test_serialize_aware_datetime(self):
782         t = Task(self.tw, description="task1", due=self.localtime_aware)
783         self.assertEqual(json.loads(t.export_data())['due'], 
784                          self.utctime_aware.strftime(DATE_FORMAT))
785
786 class AnnotationTest(TasklibTest):
787
788     def setUp(self):
789         super(AnnotationTest, self).setUp()
790         Task(self.tw, description="test task").save()
791
792     def test_adding_annotation(self):
793         task = self.tw.tasks.get()
794         task.add_annotation('test annotation')
795         self.assertEqual(len(task['annotations']), 1)
796         ann = task['annotations'][0]
797         self.assertEqual(ann['description'], 'test annotation')
798
799     def test_removing_annotation(self):
800         task = self.tw.tasks.get()
801         task.add_annotation('test annotation')
802         ann = task['annotations'][0]
803         ann.remove()
804         self.assertEqual(len(task['annotations']), 0)
805
806     def test_removing_annotation_by_description(self):
807         task = self.tw.tasks.get()
808         task.add_annotation('test annotation')
809         task.remove_annotation('test annotation')
810         self.assertEqual(len(task['annotations']), 0)
811
812     def test_removing_annotation_by_obj(self):
813         task = self.tw.tasks.get()
814         task.add_annotation('test annotation')
815         ann = task['annotations'][0]
816         task.remove_annotation(ann)
817         self.assertEqual(len(task['annotations']), 0)
818
819     def test_annotation_after_modification(self):
820          task = self.tw.tasks.get()
821          task['project'] = 'test'
822          task.add_annotation('I should really do this task')
823          self.assertEqual(task['project'], 'test')
824          task.save()
825          self.assertEqual(task['project'], 'test')
826
827     def test_serialize_annotations(self):
828         # Test that serializing annotations is possible
829         t = Task(self.tw, description="test")
830         t.save()
831
832         t.add_annotation("annotation1")
833         t.add_annotation("annotation2")
834
835         data = t._serialize('annotations', t._data['annotations'])
836
837         self.assertEqual(len(data), 2)
838         self.assertEqual(type(data[0]), dict)
839         self.assertEqual(type(data[1]), dict)
840
841         self.assertEqual(data[0]['description'], "annotation1")
842         self.assertEqual(data[1]['description'], "annotation2")
843
844
845 class UnicodeTest(TasklibTest):
846
847     def test_unicode_task(self):
848         Task(self.tw, description="†åßk").save()
849         self.tw.tasks.get()
850
851     def test_non_unicode_task(self):
852         Task(self.tw, description="test task").save()
853         self.tw.tasks.get()
854
855 class ReadOnlyDictViewTest(unittest.TestCase):
856
857     def setUp(self):
858         self.sample = dict(l=[1,2,3], d={'k':'v'})
859         self.original_sample = copy.deepcopy(self.sample)
860         self.view = ReadOnlyDictView(self.sample)
861
862     def test_readonlydictview_getitem(self):
863         l = self.view['l']
864         self.assertEqual(l, self.sample['l'])
865
866         # Assert that modification changed only copied value
867         l.append(4)
868         self.assertNotEqual(l, self.sample['l'])
869
870         # Assert that viewed dict is not changed
871         self.assertEqual(self.sample, self.original_sample)
872
873     def test_readonlydictview_contains(self):
874         self.assertEqual('l' in self.view, 'l' in self.sample)
875         self.assertEqual('d' in self.view, 'd' in self.sample)
876         self.assertEqual('k' in self.view, 'k' in self.sample)
877
878         # Assert that viewed dict is not changed
879         self.assertEqual(self.sample, self.original_sample)
880
881     def test_readonlydictview_iter(self):
882         self.assertEqual(list(k for k in self.view),
883                          list(k for k in self.sample))
884
885         # Assert the view is correct after modification
886         self.sample['new'] = 'value'
887         self.assertEqual(list(k for k in self.view),
888                          list(k for k in self.sample))
889
890     def test_readonlydictview_len(self):
891         self.assertEqual(len(self.view), len(self.sample))
892
893         # Assert the view is correct after modification
894         self.sample['new'] = 'value'
895         self.assertEqual(len(self.view), len(self.sample))
896
897     def test_readonlydictview_get(self):
898         l = self.view.get('l')
899         self.assertEqual(l, self.sample.get('l'))
900
901         # Assert that modification changed only copied value
902         l.append(4)
903         self.assertNotEqual(l, self.sample.get('l'))
904
905         # Assert that viewed dict is not changed
906         self.assertEqual(self.sample, self.original_sample)
907
908     def test_readonlydict_items(self):
909         view_items = self.view.items()
910         sample_items = list(self.sample.items())
911         self.assertEqual(view_items, sample_items)
912
913         view_items.append('newkey')
914         self.assertNotEqual(view_items, sample_items)
915         self.assertEqual(self.sample, self.original_sample)
916
917     def test_readonlydict_values(self):
918         view_values = self.view.values()
919         sample_values = list(self.sample.values())
920         self.assertEqual(view_values, sample_values)
921
922         view_list_item = list(filter(lambda x: type(x) is list,
923                                      view_values))[0]
924         view_list_item.append(4)
925         self.assertNotEqual(view_values, sample_values)
926         self.assertEqual(self.sample, self.original_sample)