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

tests: Test task normalizers in more sensible way
[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)
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 class TaskTest(TasklibTest):
133
134     def test_create_unsaved_task(self):
135         # Make sure a new task is not saved unless explicitly called for
136         t = Task(self.tw, description="test task")
137         self.assertEqual(len(self.tw.tasks.all()), 0)
138
139     # TODO: once python 2.6 compatiblity is over, use context managers here
140     #       and in all subsequent tests for assertRaises
141
142     def test_delete_unsaved_task(self):
143         t = Task(self.tw, description="test task")
144         self.assertRaises(Task.NotSaved, t.delete)
145
146     def test_complete_unsaved_task(self):
147         t = Task(self.tw, description="test task")
148         self.assertRaises(Task.NotSaved, t.done)
149
150     def test_refresh_unsaved_task(self):
151         t = Task(self.tw, description="test task")
152         self.assertRaises(Task.NotSaved, t.refresh)
153
154     def test_delete_deleted_task(self):
155         t = Task(self.tw, description="test task")
156         t.save()
157         t.delete()
158
159         self.assertRaises(Task.DeletedTask, t.delete)
160
161     def test_complete_completed_task(self):
162         t = Task(self.tw, description="test task")
163         t.save()
164         t.done()
165
166         self.assertRaises(Task.CompletedTask, t.done)
167
168     def test_complete_deleted_task(self):
169         t = Task(self.tw, description="test task")
170         t.save()
171         t.delete()
172
173         self.assertRaises(Task.DeletedTask, t.done)
174
175     def test_modify_simple_attribute_without_space(self):
176         t = Task(self.tw, description="test")
177         t.save()
178
179         self.assertEquals(t['description'], "test")
180
181         t['description'] = "test-modified"
182         t.save()
183
184         self.assertEquals(t['description'], "test-modified")
185
186     def test_modify_simple_attribute_with_space(self):
187         # Space can pose problems with parsing
188         t = Task(self.tw, description="test task")
189         t.save()
190
191         self.assertEquals(t['description'], "test task")
192
193         t['description'] = "test task modified"
194         t.save()
195
196         self.assertEquals(t['description'], "test task modified")
197
198     def test_empty_dependency_set_of_unsaved_task(self):
199         t = Task(self.tw, description="test task")
200         self.assertEqual(t['depends'], set())
201
202     def test_empty_dependency_set_of_saved_task(self):
203         t = Task(self.tw, description="test task")
204         t.save()
205         self.assertEqual(t['depends'], set())
206
207     def test_set_unsaved_task_as_dependency(self):
208         # Adds only one dependency to task with no dependencies
209         t = Task(self.tw, description="test task")
210         dependency = Task(self.tw, description="needs to be done first")
211
212         # We only save the parent task, dependency task is unsaved
213         t.save()
214         t['depends'] = set([dependency])
215
216         self.assertRaises(Task.NotSaved, t.save)
217
218     def test_set_simple_dependency_set(self):
219         # Adds only one dependency to task with no dependencies
220         t = Task(self.tw, description="test task")
221         dependency = Task(self.tw, description="needs to be done first")
222
223         t.save()
224         dependency.save()
225
226         t['depends'] = set([dependency])
227
228         self.assertEqual(t['depends'], set([dependency]))
229
230     def test_set_complex_dependency_set(self):
231         # Adds two dependencies to task with no dependencies
232         t = Task(self.tw, description="test task")
233         dependency1 = Task(self.tw, description="needs to be done first")
234         dependency2 = Task(self.tw, description="needs to be done second")
235
236         t.save()
237         dependency1.save()
238         dependency2.save()
239
240         t['depends'] = set([dependency1, dependency2])
241
242         self.assertEqual(t['depends'], set([dependency1, dependency2]))
243
244     def test_remove_from_dependency_set(self):
245         # Removes dependency from task with two dependencies
246         t = Task(self.tw, description="test task")
247         dependency1 = Task(self.tw, description="needs to be done first")
248         dependency2 = Task(self.tw, description="needs to be done second")
249
250         dependency1.save()
251         dependency2.save()
252
253         t['depends'] = set([dependency1, dependency2])
254         t.save()
255
256         t['depends'].remove(dependency2)
257         t.save()
258
259         self.assertEqual(t['depends'], set([dependency1]))
260
261     def test_add_to_dependency_set(self):
262         # Adds dependency to task with one dependencies
263         t = Task(self.tw, description="test task")
264         dependency1 = Task(self.tw, description="needs to be done first")
265         dependency2 = Task(self.tw, description="needs to be done second")
266
267         dependency1.save()
268         dependency2.save()
269
270         t['depends'] = set([dependency1])
271         t.save()
272
273         t['depends'].add(dependency2)
274         t.save()
275
276         self.assertEqual(t['depends'], set([dependency1, dependency2]))
277
278     def test_add_to_empty_dependency_set(self):
279         # Adds dependency to task with one dependencies
280         t = Task(self.tw, description="test task")
281         dependency = Task(self.tw, description="needs to be done first")
282
283         dependency.save()
284
285         t['depends'].add(dependency)
286         t.save()
287
288         self.assertEqual(t['depends'], set([dependency]))
289
290     def test_simple_dependency_set_save_repeatedly(self):
291         # Adds only one dependency to task with no dependencies
292         t = Task(self.tw, description="test task")
293         dependency = Task(self.tw, description="needs to be done first")
294         dependency.save()
295
296         t['depends'] = set([dependency])
297         t.save()
298
299         # We taint the task, but keep depends intact
300         t['description'] = "test task modified"
301         t.save()
302
303         self.assertEqual(t['depends'], set([dependency]))
304
305         # We taint the task, but assign the same set to the depends
306         t['depends'] = set([dependency])
307         t['description'] = "test task modified again"
308         t.save()
309
310         self.assertEqual(t['depends'], set([dependency]))
311
312     def test_compare_different_tasks(self):
313         # Negative: compare two different tasks
314         t1 = Task(self.tw, description="test task")
315         t2 = Task(self.tw, description="test task")
316
317         t1.save()
318         t2.save()
319
320         self.assertEqual(t1 == t2, False)
321
322     def test_compare_same_task_object(self):
323         # Compare Task object wit itself
324         t = Task(self.tw, description="test task")
325         t.save()
326
327         self.assertEqual(t == t, True)
328
329     def test_compare_same_task(self):
330         # Compare the same task using two different objects
331         t1 = Task(self.tw, description="test task")
332         t1.save()
333
334         t2 = self.tw.tasks.get(uuid=t1['uuid'])
335         self.assertEqual(t1 == t2, True)
336
337     def test_compare_unsaved_tasks(self):
338         # t1 and t2 are unsaved tasks, considered to be unequal
339         # despite the content of data
340         t1 = Task(self.tw, description="test task")
341         t2 = Task(self.tw, description="test task")
342
343         self.assertEqual(t1 == t2, False)
344
345     def test_hash_unsaved_tasks(self):
346         # Considered equal, it's the same object
347         t1 = Task(self.tw, description="test task")
348         t2 = t1
349         self.assertEqual(hash(t1) == hash(t2), True)
350
351     def test_hash_same_task(self):
352         # Compare the hash of the task using two different objects
353         t1 = Task(self.tw, description="test task")
354         t1.save()
355
356         t2 = self.tw.tasks.get(uuid=t1['uuid'])
357         self.assertEqual(t1.__hash__(), t2.__hash__())
358
359     def test_adding_task_with_priority(self):
360         t = Task(self.tw, description="test task", priority="M")
361         t.save()
362
363     def test_removing_priority_with_none(self):
364         t = Task(self.tw, description="test task", priority="L")
365         t.save()
366
367         # Remove the priority mark
368         t['priority'] = None
369         t.save()
370
371         # Assert that priority is not there after saving
372         self.assertEqual(t['priority'], None)
373
374     def test_adding_task_with_due_time(self):
375         t = Task(self.tw, description="test task", due=datetime.datetime.now())
376         t.save()
377
378     def test_removing_due_time_with_none(self):
379         t = Task(self.tw, description="test task", due=datetime.datetime.now())
380         t.save()
381
382         # Remove the due timestamp
383         t['due'] = None
384         t.save()
385
386         # Assert that due timestamp is no longer there
387         self.assertEqual(t['due'], None)
388
389     def test_modified_fields_new_task(self):
390         t = Task(self.tw)
391
392         # This should be empty with new task
393         self.assertEqual(set(t._modified_fields), set())
394
395         # Modify the task
396         t['description'] = "test task"
397         self.assertEqual(set(t._modified_fields), set(['description']))
398
399         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
400         self.assertEqual(set(t._modified_fields), set(['description', 'due']))
401
402         t['project'] = "test project"
403         self.assertEqual(set(t._modified_fields),
404                          set(['description', 'due', 'project']))
405
406         # List of modified fields should clear out when saved
407         t.save()
408         self.assertEqual(set(t._modified_fields), set())
409
410         # Reassigning the fields with the same values now should not produce
411         # modified fields
412         t['description'] = "test task"
413         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
414         t['project'] = "test project"
415         self.assertEqual(set(t._modified_fields), set())
416
417     def test_modified_fields_loaded_task(self):
418         t = Task(self.tw)
419
420         # Modify the task
421         t['description'] = "test task"
422         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
423         t['project'] = "test project"
424
425         dependency = Task(self.tw, description="dependency")
426         dependency.save()
427         t['depends'] = set([dependency])
428
429         # List of modified fields should clear out when saved
430         t.save()
431         self.assertEqual(set(t._modified_fields), set())
432
433         # Get the task by using a filter by UUID
434         t2 = self.tw.tasks.get(uuid=t['uuid'])
435
436         # Reassigning the fields with the same values now should not produce
437         # modified fields
438         t['description'] = "test task"
439         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
440         t['project'] = "test project"
441         t['depends'] = set([dependency])
442         self.assertEqual(set(t._modified_fields), set())
443
444     def test_modified_fields_not_affected_by_reading(self):
445         t = Task(self.tw)
446
447         for field in TASK_STANDARD_ATTRS:
448             value = t[field]
449
450         self.assertEqual(set(t._modified_fields), set())
451
452     def test_setting_read_only_attrs_through_init(self):
453         # Test that we are unable to set readonly attrs through __init__
454         for readonly_key in Task.read_only_fields:
455             kwargs = {'description': 'test task', readonly_key: 'value'}
456             self.assertRaises(RuntimeError,
457                               lambda: Task(self.tw, **kwargs))
458
459     def test_setting_read_only_attrs_through_setitem(self):
460         # Test that we are unable to set readonly attrs through __init__
461         for readonly_key in Task.read_only_fields:
462             t = Task(self.tw, description='test task')
463             self.assertRaises(RuntimeError,
464                               lambda: t.__setitem__(readonly_key, 'value'))
465
466     def test_saving_unmodified_task(self):
467         t = Task(self.tw, description="test task")
468         t.save()
469         t.save()
470
471     def test_adding_tag_by_appending(self):
472         t = Task(self.tw, description="test task", tags=['test1'])
473         t.save()
474         t['tags'].append('test2')
475         t.save()
476         self.assertEqual(t['tags'], ['test1', 'test2'])
477
478     def test_adding_tag_by_appending_empty(self):
479         t = Task(self.tw, description="test task")
480         t.save()
481         t['tags'].append('test')
482         t.save()
483         self.assertEqual(t['tags'], ['test'])
484
485     def test_serializers_returning_empty_string_for_none(self):
486         # Test that any serializer returns '' when passed None
487         t = Task(self.tw)
488         serializers = [getattr(t, serializer_name) for serializer_name in
489                        filter(lambda x: x.startswith('serialize_'), dir(t))]
490         for serializer in serializers:
491             self.assertEqual(serializer(None), '')
492
493     def test_deserializer_returning_empty_value_for_empty_string(self):
494         # Test that any deserializer returns empty value when passed ''
495         t = Task(self.tw)
496         deserializers = [getattr(t, deserializer_name) for deserializer_name in
497                         filter(lambda x: x.startswith('deserialize_'), dir(t))]
498         for deserializer in deserializers:
499             self.assertTrue(deserializer('') in (None, [], set()))
500
501     def test_normalizers_handling_none(self):
502         # Test that any normalizer can handle None as a valid value
503         t = Task(self.tw)
504
505         for key in TASK_STANDARD_ATTRS:
506             t._normalize(key, None)
507
508     def test_recurrent_task_generation(self):
509         today = datetime.date.today()
510         t = Task(self.tw, description="brush teeth",
511                  due=today, recur="daily")
512         t.save()
513         self.assertEqual(len(self.tw.tasks.pending()), 2)
514
515 class TaskFromHookTest(TasklibTest):
516
517     input_add_data = six.StringIO(
518         '{"description":"Buy some milk",'
519         '"entry":"20141118T050231Z",'
520         '"status":"pending",'
521         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
522
523     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
524         '{"description":"Buy some milk finally",'
525         '"entry":"20141118T050231Z",'
526         '"status":"completed",'
527         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
528
529     exported_raw_data = (
530         '{"project":"Home",'
531          '"due":"20150101T232323Z",'
532          '"description":"test task"}')
533
534     def test_setting_up_from_add_hook_input(self):
535         t = Task.from_input(input_file=self.input_add_data, warrior=self.tw)
536         self.assertEqual(t['description'], "Buy some milk")
537         self.assertEqual(t.pending, True)
538
539     def test_setting_up_from_modified_hook_input(self):
540         t = Task.from_input(input_file=self.input_modify_data, modify=True,
541                             warrior=self.tw)
542         self.assertEqual(t['description'], "Buy some milk finally")
543         self.assertEqual(t.pending, False)
544         self.assertEqual(t.completed, True)
545
546         self.assertEqual(t._original_data['status'], "pending")
547         self.assertEqual(t._original_data['description'], "Buy some milk")
548         self.assertEqual(set(t._modified_fields),
549                          set(['status', 'description']))
550
551     def test_export_data(self):
552         t = Task(self.tw, description="test task",
553             project="Home",
554             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
555
556         # Check that the output is a permutation of:
557         # {"project":"Home","description":"test task","due":"20150101232323Z"}
558         allowed_segments = self.exported_raw_data[1:-1].split(',')
559         allowed_output = [
560             '{' + ','.join(segments) + '}'
561             for segments in itertools.permutations(allowed_segments)
562         ]
563
564         self.assertTrue(any(t.export_data() == expected
565                             for expected in allowed_output))
566
567 class TimezoneAwareDatetimeTest(TasklibTest):
568
569     def setUp(self):
570         super(TimezoneAwareDatetimeTest, self).setUp()
571         self.zone = local_zone
572         self.localdate_naive = datetime.datetime(2015,2,2)
573         self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
574         self.localtime_aware = self.zone.localize(self.localtime_naive)
575         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
576
577     def test_timezone_naive_datetime_setitem(self):
578         t = Task(self.tw, description="test task")
579         t['due'] = self.localtime_naive
580         self.assertEqual(t['due'], self.localtime_aware)
581
582     def test_timezone_naive_datetime_using_init(self):
583         t = Task(self.tw, description="test task", due=self.localtime_naive)
584         self.assertEqual(t['due'], self.localtime_aware)
585
586     def test_filter_by_naive_datetime(self):
587         t = Task(self.tw, description="task1", due=self.localtime_naive)
588         t.save()
589         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
590         self.assertEqual(len(matching_tasks), 1)
591
592     def test_serialize_naive_datetime(self):
593         t = Task(self.tw, description="task1", due=self.localtime_naive)
594         self.assertEqual(json.loads(t.export_data())['due'], 
595                          self.utctime_aware.strftime(DATE_FORMAT))
596
597     def test_timezone_naive_date_setitem(self):
598         t = Task(self.tw, description="test task")
599         t['due'] = self.localdate_naive
600         self.assertEqual(t['due'], self.localtime_aware)
601
602     def test_timezone_naive_date_using_init(self):
603         t = Task(self.tw, description="test task", due=self.localdate_naive)
604         self.assertEqual(t['due'], self.localtime_aware)
605
606     def test_filter_by_naive_date(self):
607         t = Task(self.tw, description="task1", due=self.localdate_naive)
608         t.save()
609         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
610         self.assertEqual(len(matching_tasks), 1)
611
612     def test_serialize_naive_date(self):
613         t = Task(self.tw, description="task1", due=self.localdate_naive)
614         self.assertEqual(json.loads(t.export_data())['due'], 
615                          self.utctime_aware.strftime(DATE_FORMAT))
616
617     def test_timezone_aware_datetime_setitem(self):
618         t = Task(self.tw, description="test task")
619         t['due'] = self.localtime_aware
620         self.assertEqual(t['due'], self.localtime_aware)
621
622     def test_timezone_aware_datetime_using_init(self):
623         t = Task(self.tw, description="test task", due=self.localtime_aware)
624         self.assertEqual(t['due'], self.localtime_aware)
625
626     def test_filter_by_aware_datetime(self):
627         t = Task(self.tw, description="task1", due=self.localtime_aware)
628         t.save()
629         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
630         self.assertEqual(len(matching_tasks), 1)
631
632     def test_serialize_aware_datetime(self):
633         t = Task(self.tw, description="task1", due=self.localtime_aware)
634         self.assertEqual(json.loads(t.export_data())['due'], 
635                          self.utctime_aware.strftime(DATE_FORMAT))
636
637 class AnnotationTest(TasklibTest):
638
639     def setUp(self):
640         super(AnnotationTest, self).setUp()
641         Task(self.tw, description="test task").save()
642
643     def test_adding_annotation(self):
644         task = self.tw.tasks.get()
645         task.add_annotation('test annotation')
646         self.assertEqual(len(task['annotations']), 1)
647         ann = task['annotations'][0]
648         self.assertEqual(ann['description'], 'test annotation')
649
650     def test_removing_annotation(self):
651         task = self.tw.tasks.get()
652         task.add_annotation('test annotation')
653         ann = task['annotations'][0]
654         ann.remove()
655         self.assertEqual(len(task['annotations']), 0)
656
657     def test_removing_annotation_by_description(self):
658         task = self.tw.tasks.get()
659         task.add_annotation('test annotation')
660         task.remove_annotation('test annotation')
661         self.assertEqual(len(task['annotations']), 0)
662
663     def test_removing_annotation_by_obj(self):
664         task = self.tw.tasks.get()
665         task.add_annotation('test annotation')
666         ann = task['annotations'][0]
667         task.remove_annotation(ann)
668         self.assertEqual(len(task['annotations']), 0)
669
670     def test_annotation_after_modification(self):
671          task = self.tw.tasks.get()
672          task['project'] = 'test'
673          task.add_annotation('I should really do this task')
674          self.assertEqual(task['project'], 'test')
675          task.save()
676          self.assertEqual(task['project'], 'test')
677
678     def test_serialize_annotations(self):
679         # Test that serializing annotations is possible
680         t = Task(self.tw, description="test")
681         t.save()
682
683         t.add_annotation("annotation1")
684         t.add_annotation("annotation2")
685
686         data = t._serialize('annotations', t._data['annotations'])
687
688         self.assertEqual(len(data), 2)
689         self.assertEqual(type(data[0]), dict)
690         self.assertEqual(type(data[1]), dict)
691
692         self.assertEqual(data[0]['description'], "annotation1")
693         self.assertEqual(data[1]['description'], "annotation2")
694
695
696 class UnicodeTest(TasklibTest):
697
698     def test_unicode_task(self):
699         Task(self.tw, description="†åßk").save()
700         self.tw.tasks.get()
701
702     def test_non_unicode_task(self):
703         Task(self.tw, description="test task").save()
704         self.tw.tasks.get()
705
706 class ReadOnlyDictViewTest(unittest.TestCase):
707
708     def setUp(self):
709         self.sample = dict(l=[1,2,3], d={'k':'v'})
710         self.original_sample = copy.deepcopy(self.sample)
711         self.view = ReadOnlyDictView(self.sample)
712
713     def test_readonlydictview_getitem(self):
714         l = self.view['l']
715         self.assertEqual(l, self.sample['l'])
716
717         # Assert that modification changed only copied value
718         l.append(4)
719         self.assertNotEqual(l, self.sample['l'])
720
721         # Assert that viewed dict is not changed
722         self.assertEqual(self.sample, self.original_sample)
723
724     def test_readonlydictview_contains(self):
725         self.assertEqual('l' in self.view, 'l' in self.sample)
726         self.assertEqual('d' in self.view, 'd' in self.sample)
727         self.assertEqual('k' in self.view, 'k' in self.sample)
728
729         # Assert that viewed dict is not changed
730         self.assertEqual(self.sample, self.original_sample)
731
732     def test_readonlydictview_iter(self):
733         self.assertEqual(list(k for k in self.view),
734                          list(k for k in self.sample))
735
736         # Assert the view is correct after modification
737         self.sample['new'] = 'value'
738         self.assertEqual(list(k for k in self.view),
739                          list(k for k in self.sample))
740
741     def test_readonlydictview_len(self):
742         self.assertEqual(len(self.view), len(self.sample))
743
744         # Assert the view is correct after modification
745         self.sample['new'] = 'value'
746         self.assertEqual(len(self.view), len(self.sample))
747
748     def test_readonlydictview_get(self):
749         l = self.view.get('l')
750         self.assertEqual(l, self.sample.get('l'))
751
752         # Assert that modification changed only copied value
753         l.append(4)
754         self.assertNotEqual(l, self.sample.get('l'))
755
756         # Assert that viewed dict is not changed
757         self.assertEqual(self.sample, self.original_sample)
758
759     def test_readonlydict_items(self):
760         view_items = self.view.items()
761         sample_items = list(self.sample.items())
762         self.assertEqual(view_items, sample_items)
763
764         view_items.append('newkey')
765         self.assertNotEqual(view_items, sample_items)
766         self.assertEqual(self.sample, self.original_sample)
767
768     def test_readonlydict_values(self):
769         view_values = self.view.values()
770         sample_values = list(self.sample.values())
771         self.assertEqual(view_values, sample_values)
772
773         view_list_item = list(filter(lambda x: type(x) is list,
774                                      view_values))[0]
775         view_list_item.append(4)
776         self.assertNotEqual(view_values, sample_values)
777         self.assertEqual(self.sample, self.original_sample)