]> 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: Detect string type using six properly
[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         normalizers = [getattr(t, normalizer_name) for normalizer_name in
506                        filter(lambda x: x.startswith('normalize_'), dir(t))]
507
508         for normalizer in normalizers:
509             normalizer(None)
510
511     def test_recurrent_task_generation(self):
512         today = datetime.date.today()
513         t = Task(self.tw, description="brush teeth",
514                  due=today, recur="daily")
515         t.save()
516         self.assertEqual(len(self.tw.tasks.pending()), 2)
517
518 class TaskFromHookTest(TasklibTest):
519
520     input_add_data = six.StringIO(
521         '{"description":"Buy some milk",'
522         '"entry":"20141118T050231Z",'
523         '"status":"pending",'
524         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
525
526     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
527         '{"description":"Buy some milk finally",'
528         '"entry":"20141118T050231Z",'
529         '"status":"completed",'
530         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
531
532     exported_raw_data = (
533         '{"project":"Home",'
534          '"due":"20150101T232323Z",'
535          '"description":"test task"}')
536
537     def test_setting_up_from_add_hook_input(self):
538         t = Task.from_input(input_file=self.input_add_data)
539         self.assertEqual(t['description'], "Buy some milk")
540         self.assertEqual(t.pending, True)
541
542     def test_setting_up_from_modified_hook_input(self):
543         t = Task.from_input(input_file=self.input_modify_data, modify=True)
544         self.assertEqual(t['description'], "Buy some milk finally")
545         self.assertEqual(t.pending, False)
546         self.assertEqual(t.completed, True)
547
548         self.assertEqual(t._original_data['status'], "pending")
549         self.assertEqual(t._original_data['description'], "Buy some milk")
550         self.assertEqual(set(t._modified_fields),
551                          set(['status', 'description']))
552
553     def test_export_data(self):
554         t = Task(self.tw, description="test task",
555             project="Home",
556             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
557
558         # Check that the output is a permutation of:
559         # {"project":"Home","description":"test task","due":"20150101232323Z"}
560         allowed_segments = self.exported_raw_data[1:-1].split(',')
561         allowed_output = [
562             '{' + ','.join(segments) + '}'
563             for segments in itertools.permutations(allowed_segments)
564         ]
565
566         self.assertTrue(any(t.export_data() == expected
567                             for expected in allowed_output))
568
569 class TimezoneAwareDatetimeTest(TasklibTest):
570
571     def setUp(self):
572         super(TimezoneAwareDatetimeTest, self).setUp()
573         self.zone = local_zone
574         self.localdate_naive = datetime.datetime(2015,2,2)
575         self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
576         self.localtime_aware = self.zone.localize(self.localtime_naive)
577         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
578
579     def test_timezone_naive_datetime_setitem(self):
580         t = Task(self.tw, description="test task")
581         t['due'] = self.localtime_naive
582         self.assertEqual(t['due'], self.localtime_aware)
583
584     def test_timezone_naive_datetime_using_init(self):
585         t = Task(self.tw, description="test task", due=self.localtime_naive)
586         self.assertEqual(t['due'], self.localtime_aware)
587
588     def test_filter_by_naive_datetime(self):
589         t = Task(self.tw, description="task1", due=self.localtime_naive)
590         t.save()
591         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
592         self.assertEqual(len(matching_tasks), 1)
593
594     def test_serialize_naive_datetime(self):
595         t = Task(self.tw, description="task1", due=self.localtime_naive)
596         self.assertEqual(json.loads(t.export_data())['due'], 
597                          self.utctime_aware.strftime(DATE_FORMAT))
598
599     def test_timezone_naive_date_setitem(self):
600         t = Task(self.tw, description="test task")
601         t['due'] = self.localdate_naive
602         self.assertEqual(t['due'], self.localtime_aware)
603
604     def test_timezone_naive_date_using_init(self):
605         t = Task(self.tw, description="test task", due=self.localdate_naive)
606         self.assertEqual(t['due'], self.localtime_aware)
607
608     def test_filter_by_naive_date(self):
609         t = Task(self.tw, description="task1", due=self.localdate_naive)
610         t.save()
611         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
612         self.assertEqual(len(matching_tasks), 1)
613
614     def test_serialize_naive_date(self):
615         t = Task(self.tw, description="task1", due=self.localdate_naive)
616         self.assertEqual(json.loads(t.export_data())['due'], 
617                          self.utctime_aware.strftime(DATE_FORMAT))
618
619     def test_timezone_aware_datetime_setitem(self):
620         t = Task(self.tw, description="test task")
621         t['due'] = self.localtime_aware
622         self.assertEqual(t['due'], self.localtime_aware)
623
624     def test_timezone_aware_datetime_using_init(self):
625         t = Task(self.tw, description="test task", due=self.localtime_aware)
626         self.assertEqual(t['due'], self.localtime_aware)
627
628     def test_filter_by_aware_datetime(self):
629         t = Task(self.tw, description="task1", due=self.localtime_aware)
630         t.save()
631         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
632         self.assertEqual(len(matching_tasks), 1)
633
634     def test_serialize_aware_datetime(self):
635         t = Task(self.tw, description="task1", due=self.localtime_aware)
636         self.assertEqual(json.loads(t.export_data())['due'], 
637                          self.utctime_aware.strftime(DATE_FORMAT))
638
639 class AnnotationTest(TasklibTest):
640
641     def setUp(self):
642         super(AnnotationTest, self).setUp()
643         Task(self.tw, description="test task").save()
644
645     def test_adding_annotation(self):
646         task = self.tw.tasks.get()
647         task.add_annotation('test annotation')
648         self.assertEqual(len(task['annotations']), 1)
649         ann = task['annotations'][0]
650         self.assertEqual(ann['description'], 'test annotation')
651
652     def test_removing_annotation(self):
653         task = self.tw.tasks.get()
654         task.add_annotation('test annotation')
655         ann = task['annotations'][0]
656         ann.remove()
657         self.assertEqual(len(task['annotations']), 0)
658
659     def test_removing_annotation_by_description(self):
660         task = self.tw.tasks.get()
661         task.add_annotation('test annotation')
662         task.remove_annotation('test annotation')
663         self.assertEqual(len(task['annotations']), 0)
664
665     def test_removing_annotation_by_obj(self):
666         task = self.tw.tasks.get()
667         task.add_annotation('test annotation')
668         ann = task['annotations'][0]
669         task.remove_annotation(ann)
670         self.assertEqual(len(task['annotations']), 0)
671
672     def test_annotation_after_modification(self):
673          task = self.tw.tasks.get()
674          task['project'] = 'test'
675          task.add_annotation('I should really do this task')
676          self.assertEqual(task['project'], 'test')
677          task.save()
678          self.assertEqual(task['project'], 'test')
679
680     def test_serialize_annotations(self):
681         # Test that serializing annotations is possible
682         t = Task(self.tw, description="test")
683         t.save()
684
685         t.add_annotation("annotation1")
686         t.add_annotation("annotation2")
687
688         data = t._serialize('annotations', t._data['annotations'])
689
690         self.assertEqual(len(data), 2)
691         self.assertEqual(type(data[0]), dict)
692         self.assertEqual(type(data[1]), dict)
693
694         self.assertEqual(data[0]['description'], "annotation1")
695         self.assertEqual(data[1]['description'], "annotation2")
696
697
698 class UnicodeTest(TasklibTest):
699
700     def test_unicode_task(self):
701         Task(self.tw, description="†åßk").save()
702         self.tw.tasks.get()
703
704     def test_non_unicode_task(self):
705         Task(self.tw, description="test task").save()
706         self.tw.tasks.get()
707
708 class ReadOnlyDictViewTest(unittest.TestCase):
709
710     def setUp(self):
711         self.sample = dict(l=[1,2,3], d={'k':'v'})
712         self.original_sample = copy.deepcopy(self.sample)
713         self.view = ReadOnlyDictView(self.sample)
714
715     def test_readonlydictview_getitem(self):
716         l = self.view['l']
717         self.assertEqual(l, self.sample['l'])
718
719         # Assert that modification changed only copied value
720         l.append(4)
721         self.assertNotEqual(l, self.sample['l'])
722
723         # Assert that viewed dict is not changed
724         self.assertEqual(self.sample, self.original_sample)
725
726     def test_readonlydictview_contains(self):
727         self.assertEqual('l' in self.view, 'l' in self.sample)
728         self.assertEqual('d' in self.view, 'd' in self.sample)
729         self.assertEqual('k' in self.view, 'k' in self.sample)
730
731         # Assert that viewed dict is not changed
732         self.assertEqual(self.sample, self.original_sample)
733
734     def test_readonlydictview_iter(self):
735         self.assertEqual(list(k for k in self.view),
736                          list(k for k in self.sample))
737
738         # Assert the view is correct after modification
739         self.sample['new'] = 'value'
740         self.assertEqual(list(k for k in self.view),
741                          list(k for k in self.sample))
742
743     def test_readonlydictview_len(self):
744         self.assertEqual(len(self.view), len(self.sample))
745
746         # Assert the view is correct after modification
747         self.sample['new'] = 'value'
748         self.assertEqual(len(self.view), len(self.sample))
749
750     def test_readonlydictview_get(self):
751         l = self.view.get('l')
752         self.assertEqual(l, self.sample.get('l'))
753
754         # Assert that modification changed only copied value
755         l.append(4)
756         self.assertNotEqual(l, self.sample.get('l'))
757
758         # Assert that viewed dict is not changed
759         self.assertEqual(self.sample, self.original_sample)
760
761     def test_readonlydict_items(self):
762         view_items = self.view.items()
763         sample_items = list(self.sample.items())
764         self.assertEqual(view_items, sample_items)
765
766         view_items.append('newkey')
767         self.assertNotEqual(view_items, sample_items)
768         self.assertEqual(self.sample, self.original_sample)
769
770     def test_readonlydict_values(self):
771         view_values = self.view.values()
772         sample_values = list(self.sample.values())
773         self.assertEqual(view_values, sample_values)
774
775         view_list_item = list(filter(lambda x: type(x) is list,
776                                      view_values))[0]
777         view_list_item.append(4)
778         self.assertNotEqual(view_values, sample_values)
779         self.assertEqual(self.sample, self.original_sample)