]> 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 generation of recurrent tasks
[etc/taskwarrior.git] / tasklib / tests.py
1 # coding=utf-8
2
3 import datetime
4 import itertools
5 import json
6 import pytz
7 import six
8 import shutil
9 import tempfile
10 import unittest
11
12 from .task import TaskWarrior, Task, local_zone, DATE_FORMAT
13
14 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
15 TASK_STANDARD_ATTRS = (
16     'status',
17     'uuid',
18     'entry',
19     'description',
20     'start',
21     'end',
22     'due',
23     'until',
24     'wait',
25     'modified',
26     'scheduled',
27     'recur',
28     'mask',
29     'imask',
30     'parent',
31     'project',
32     'priority',
33     'depends',
34     'tags',
35     'annotations',
36 )
37
38 class TasklibTest(unittest.TestCase):
39
40     def setUp(self):
41         self.tmp = tempfile.mkdtemp(dir='.')
42         self.tw = TaskWarrior(data_location=self.tmp)
43
44     def tearDown(self):
45         shutil.rmtree(self.tmp)
46
47
48 class TaskFilterTest(TasklibTest):
49
50     def test_all_empty(self):
51         self.assertEqual(len(self.tw.tasks.all()), 0)
52
53     def test_all_non_empty(self):
54         Task(self.tw, description="test task").save()
55         self.assertEqual(len(self.tw.tasks.all()), 1)
56         self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
57         self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
58
59     def test_pending_non_empty(self):
60         Task(self.tw, description="test task").save()
61         self.assertEqual(len(self.tw.tasks.pending()), 1)
62         self.assertEqual(self.tw.tasks.pending()[0]['description'],
63                          'test task')
64         self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
65
66     def test_completed_empty(self):
67         Task(self.tw, description="test task").save()
68         self.assertEqual(len(self.tw.tasks.completed()), 0)
69
70     def test_completed_non_empty(self):
71         Task(self.tw, description="test task").save()
72         self.assertEqual(len(self.tw.tasks.completed()), 0)
73         self.tw.tasks.all()[0].done()
74         self.assertEqual(len(self.tw.tasks.completed()), 1)
75
76     def test_filtering_by_attribute(self):
77         Task(self.tw, description="no priority task").save()
78         Task(self.tw, priority="H", description="high priority task").save()
79         self.assertEqual(len(self.tw.tasks.all()), 2)
80
81         # Assert that the correct number of tasks is returned
82         self.assertEqual(len(self.tw.tasks.filter(priority="H")), 1)
83
84         # Assert that the correct tasks are returned
85         high_priority_task = self.tw.tasks.get(priority="H")
86         self.assertEqual(high_priority_task['description'], "high priority task")
87
88     def test_filtering_by_empty_attribute(self):
89         Task(self.tw, description="no priority task").save()
90         Task(self.tw, priority="H", description="high priority task").save()
91         self.assertEqual(len(self.tw.tasks.all()), 2)
92
93         # Assert that the correct number of tasks is returned
94         self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
95
96         # Assert that the correct tasks are returned
97         no_priority_task = self.tw.tasks.get(priority=None)
98         self.assertEqual(no_priority_task['description'], "no priority task")
99
100     def test_filter_for_task_with_space_in_descripition(self):
101         task = Task(self.tw, description="test task")
102         task.save()
103
104         filtered_task = self.tw.tasks.get(description="test task")
105         self.assertEqual(filtered_task['description'], "test task")
106
107     def test_filter_for_task_without_space_in_descripition(self):
108         task = Task(self.tw, description="test")
109         task.save()
110
111         filtered_task = self.tw.tasks.get(description="test")
112         self.assertEqual(filtered_task['description'], "test")
113
114     def test_filter_for_task_with_space_in_project(self):
115         task = Task(self.tw, description="test", project="random project")
116         task.save()
117
118         filtered_task = self.tw.tasks.get(project="random project")
119         self.assertEqual(filtered_task['project'], "random project")
120
121     def test_filter_for_task_without_space_in_project(self):
122         task = Task(self.tw, description="test", project="random")
123         task.save()
124
125         filtered_task = self.tw.tasks.get(project="random")
126         self.assertEqual(filtered_task['project'], "random")
127
128
129 class TaskTest(TasklibTest):
130
131     def test_create_unsaved_task(self):
132         # Make sure a new task is not saved unless explicitly called for
133         t = Task(self.tw, description="test task")
134         self.assertEqual(len(self.tw.tasks.all()), 0)
135
136     # TODO: once python 2.6 compatiblity is over, use context managers here
137     #       and in all subsequent tests for assertRaises
138
139     def test_delete_unsaved_task(self):
140         t = Task(self.tw, description="test task")
141         self.assertRaises(Task.NotSaved, t.delete)
142
143     def test_complete_unsaved_task(self):
144         t = Task(self.tw, description="test task")
145         self.assertRaises(Task.NotSaved, t.done)
146
147     def test_refresh_unsaved_task(self):
148         t = Task(self.tw, description="test task")
149         self.assertRaises(Task.NotSaved, t.refresh)
150
151     def test_delete_deleted_task(self):
152         t = Task(self.tw, description="test task")
153         t.save()
154         t.delete()
155
156         self.assertRaises(Task.DeletedTask, t.delete)
157
158     def test_complete_completed_task(self):
159         t = Task(self.tw, description="test task")
160         t.save()
161         t.done()
162
163         self.assertRaises(Task.CompletedTask, t.done)
164
165     def test_complete_deleted_task(self):
166         t = Task(self.tw, description="test task")
167         t.save()
168         t.delete()
169
170         self.assertRaises(Task.DeletedTask, t.done)
171
172     def test_modify_simple_attribute_without_space(self):
173         t = Task(self.tw, description="test")
174         t.save()
175
176         self.assertEquals(t['description'], "test")
177
178         t['description'] = "test-modified"
179         t.save()
180
181         self.assertEquals(t['description'], "test-modified")
182
183     def test_modify_simple_attribute_with_space(self):
184         # Space can pose problems with parsing
185         t = Task(self.tw, description="test task")
186         t.save()
187
188         self.assertEquals(t['description'], "test task")
189
190         t['description'] = "test task modified"
191         t.save()
192
193         self.assertEquals(t['description'], "test task modified")
194
195     def test_empty_dependency_set_of_unsaved_task(self):
196         t = Task(self.tw, description="test task")
197         self.assertEqual(t['depends'], set())
198
199     def test_empty_dependency_set_of_saved_task(self):
200         t = Task(self.tw, description="test task")
201         t.save()
202         self.assertEqual(t['depends'], set())
203
204     def test_set_unsaved_task_as_dependency(self):
205         # Adds only one dependency to task with no dependencies
206         t = Task(self.tw, description="test task")
207         dependency = Task(self.tw, description="needs to be done first")
208
209         # We only save the parent task, dependency task is unsaved
210         t.save()
211         t['depends'] = set([dependency])
212
213         self.assertRaises(Task.NotSaved, t.save)
214
215     def test_set_simple_dependency_set(self):
216         # Adds only one dependency to task with no dependencies
217         t = Task(self.tw, description="test task")
218         dependency = Task(self.tw, description="needs to be done first")
219
220         t.save()
221         dependency.save()
222
223         t['depends'] = set([dependency])
224
225         self.assertEqual(t['depends'], set([dependency]))
226
227     def test_set_complex_dependency_set(self):
228         # Adds two dependencies to task with no dependencies
229         t = Task(self.tw, description="test task")
230         dependency1 = Task(self.tw, description="needs to be done first")
231         dependency2 = Task(self.tw, description="needs to be done second")
232
233         t.save()
234         dependency1.save()
235         dependency2.save()
236
237         t['depends'] = set([dependency1, dependency2])
238
239         self.assertEqual(t['depends'], set([dependency1, dependency2]))
240
241     def test_remove_from_dependency_set(self):
242         # Removes dependency from task with two dependencies
243         t = Task(self.tw, description="test task")
244         dependency1 = Task(self.tw, description="needs to be done first")
245         dependency2 = Task(self.tw, description="needs to be done second")
246
247         dependency1.save()
248         dependency2.save()
249
250         t['depends'] = set([dependency1, dependency2])
251         t.save()
252
253         t['depends'].remove(dependency2)
254         t.save()
255
256         self.assertEqual(t['depends'], set([dependency1]))
257
258     def test_add_to_dependency_set(self):
259         # Adds dependency to task with one dependencies
260         t = Task(self.tw, description="test task")
261         dependency1 = Task(self.tw, description="needs to be done first")
262         dependency2 = Task(self.tw, description="needs to be done second")
263
264         dependency1.save()
265         dependency2.save()
266
267         t['depends'] = set([dependency1])
268         t.save()
269
270         t['depends'].add(dependency2)
271         t.save()
272
273         self.assertEqual(t['depends'], set([dependency1, dependency2]))
274
275     def test_add_to_empty_dependency_set(self):
276         # Adds dependency to task with one dependencies
277         t = Task(self.tw, description="test task")
278         dependency = Task(self.tw, description="needs to be done first")
279
280         dependency.save()
281
282         t['depends'].add(dependency)
283         t.save()
284
285         self.assertEqual(t['depends'], set([dependency]))
286
287     def test_simple_dependency_set_save_repeatedly(self):
288         # Adds only one dependency to task with no dependencies
289         t = Task(self.tw, description="test task")
290         dependency = Task(self.tw, description="needs to be done first")
291         dependency.save()
292
293         t['depends'] = set([dependency])
294         t.save()
295
296         # We taint the task, but keep depends intact
297         t['description'] = "test task modified"
298         t.save()
299
300         self.assertEqual(t['depends'], set([dependency]))
301
302         # We taint the task, but assign the same set to the depends
303         t['depends'] = set([dependency])
304         t['description'] = "test task modified again"
305         t.save()
306
307         self.assertEqual(t['depends'], set([dependency]))
308
309     def test_compare_different_tasks(self):
310         # Negative: compare two different tasks
311         t1 = Task(self.tw, description="test task")
312         t2 = Task(self.tw, description="test task")
313
314         t1.save()
315         t2.save()
316
317         self.assertEqual(t1 == t2, False)
318
319     def test_compare_same_task_object(self):
320         # Compare Task object wit itself
321         t = Task(self.tw, description="test task")
322         t.save()
323
324         self.assertEqual(t == t, True)
325
326     def test_compare_same_task(self):
327         # Compare the same task using two different objects
328         t1 = Task(self.tw, description="test task")
329         t1.save()
330
331         t2 = self.tw.tasks.get(uuid=t1['uuid'])
332         self.assertEqual(t1 == t2, True)
333
334     def test_compare_unsaved_tasks(self):
335         # t1 and t2 are unsaved tasks, considered to be unequal
336         # despite the content of data
337         t1 = Task(self.tw, description="test task")
338         t2 = Task(self.tw, description="test task")
339
340         self.assertEqual(t1 == t2, False)
341
342     def test_hash_unsaved_tasks(self):
343         # Considered equal, it's the same object
344         t1 = Task(self.tw, description="test task")
345         t2 = t1
346         self.assertEqual(hash(t1) == hash(t2), True)
347
348     def test_hash_same_task(self):
349         # Compare the hash of the task using two different objects
350         t1 = Task(self.tw, description="test task")
351         t1.save()
352
353         t2 = self.tw.tasks.get(uuid=t1['uuid'])
354         self.assertEqual(t1.__hash__(), t2.__hash__())
355
356     def test_adding_task_with_priority(self):
357         t = Task(self.tw, description="test task", priority="M")
358         t.save()
359
360     def test_removing_priority_with_none(self):
361         t = Task(self.tw, description="test task", priority="L")
362         t.save()
363
364         # Remove the priority mark
365         t['priority'] = None
366         t.save()
367
368         # Assert that priority is not there after saving
369         self.assertEqual(t['priority'], None)
370
371     def test_adding_task_with_due_time(self):
372         t = Task(self.tw, description="test task", due=datetime.datetime.now())
373         t.save()
374
375     def test_removing_due_time_with_none(self):
376         t = Task(self.tw, description="test task", due=datetime.datetime.now())
377         t.save()
378
379         # Remove the due timestamp
380         t['due'] = None
381         t.save()
382
383         # Assert that due timestamp is no longer there
384         self.assertEqual(t['due'], None)
385
386     def test_modified_fields_new_task(self):
387         t = Task(self.tw)
388
389         # This should be empty with new task
390         self.assertEqual(set(t._modified_fields), set())
391
392         # Modify the task
393         t['description'] = "test task"
394         self.assertEqual(set(t._modified_fields), set(['description']))
395
396         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
397         self.assertEqual(set(t._modified_fields), set(['description', 'due']))
398
399         t['project'] = "test project"
400         self.assertEqual(set(t._modified_fields),
401                          set(['description', 'due', 'project']))
402
403         # List of modified fields should clear out when saved
404         t.save()
405         self.assertEqual(set(t._modified_fields), set())
406
407         # Reassigning the fields with the same values now should not produce
408         # modified fields
409         t['description'] = "test task"
410         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
411         t['project'] = "test project"
412         self.assertEqual(set(t._modified_fields), set())
413
414     def test_modified_fields_loaded_task(self):
415         t = Task(self.tw)
416
417         # Modify the task
418         t['description'] = "test task"
419         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
420         t['project'] = "test project"
421
422         dependency = Task(self.tw, description="dependency")
423         dependency.save()
424         t['depends'] = set([dependency])
425
426         # List of modified fields should clear out when saved
427         t.save()
428         self.assertEqual(set(t._modified_fields), set())
429
430         # Get the task by using a filter by UUID
431         t2 = self.tw.tasks.get(uuid=t['uuid'])
432
433         # Reassigning the fields with the same values now should not produce
434         # modified fields
435         t['description'] = "test task"
436         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
437         t['project'] = "test project"
438         t['depends'] = set([dependency])
439         self.assertEqual(set(t._modified_fields), set())
440
441     def test_modified_fields_not_affected_by_reading(self):
442         t = Task(self.tw)
443
444         for field in TASK_STANDARD_ATTRS:
445             value = t[field]
446
447         self.assertEqual(set(t._modified_fields), set())
448
449     def test_setting_read_only_attrs_through_init(self):
450         # Test that we are unable to set readonly attrs through __init__
451         for readonly_key in Task.read_only_fields:
452             kwargs = {'description': 'test task', readonly_key: 'value'}
453             self.assertRaises(RuntimeError,
454                               lambda: Task(self.tw, **kwargs))
455
456     def test_setting_read_only_attrs_through_setitem(self):
457         # Test that we are unable to set readonly attrs through __init__
458         for readonly_key in Task.read_only_fields:
459             t = Task(self.tw, description='test task')
460             self.assertRaises(RuntimeError,
461                               lambda: t.__setitem__(readonly_key, 'value'))
462
463     def test_saving_unmodified_task(self):
464         t = Task(self.tw, description="test task")
465         t.save()
466         t.save()
467
468     def test_adding_tag_by_appending(self):
469         t = Task(self.tw, description="test task", tags=['test1'])
470         t.save()
471         t['tags'].append('test2')
472         t.save()
473         self.assertEqual(t['tags'], ['test1', 'test2'])
474
475     def test_adding_tag_by_appending_empty(self):
476         t = Task(self.tw, description="test task")
477         t.save()
478         t['tags'].append('test')
479         t.save()
480         self.assertEqual(t['tags'], ['test'])
481
482     def test_serializers_returning_empty_string_for_none(self):
483         # Test that any serializer returns '' when passed None
484         t = Task(self.tw)
485         serializers = [getattr(t, serializer_name) for serializer_name in
486                        filter(lambda x: x.startswith('serialize_'), dir(t))]
487         for serializer in serializers:
488             self.assertEqual(serializer(None), '')
489
490     def test_deserializer_returning_empty_value_for_empty_string(self):
491         # Test that any deserializer returns empty value when passed ''
492         t = Task(self.tw)
493         deserializers = [getattr(t, deserializer_name) for deserializer_name in
494                         filter(lambda x: x.startswith('deserialize_'), dir(t))]
495         for deserializer in deserializers:
496             self.assertTrue(deserializer('') in (None, [], set()))
497
498     def test_normalizers_returning_empty_string_for_none(self):
499         # Test that any normalizer can handle None as a valid value
500         t = Task(self.tw)
501         normalizers = [getattr(t, normalizer_name) for normalizer_name in
502                        filter(lambda x: x.startswith('normalize_'), dir(t))]
503         for normalizer in normalizers:
504             normalizer(None)
505
506     def test_recurrent_task_generation(self):
507         today = datetime.date.today()
508         t = Task(self.tw, description="brush teeth",
509                  due=today, recur="daily")
510         t.save()
511         self.assertEqual(len(self.tw.tasks.pending()), 2)
512
513 class TaskFromHookTest(TasklibTest):
514
515     input_add_data = six.StringIO(
516         '{"description":"Buy some milk",'
517         '"entry":"20141118T050231Z",'
518         '"status":"pending",'
519         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
520
521     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
522         '{"description":"Buy some milk finally",'
523         '"entry":"20141118T050231Z",'
524         '"status":"completed",'
525         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
526
527     exported_raw_data = (
528         '{"project":"Home",'
529          '"due":"20150101T232323Z",'
530          '"description":"test task"}')
531
532     def test_setting_up_from_add_hook_input(self):
533         t = Task.from_input(input_file=self.input_add_data)
534         self.assertEqual(t['description'], "Buy some milk")
535         self.assertEqual(t.pending, True)
536
537     def test_setting_up_from_modified_hook_input(self):
538         t = Task.from_input(input_file=self.input_modify_data, modify=True)
539         self.assertEqual(t['description'], "Buy some milk finally")
540         self.assertEqual(t.pending, False)
541         self.assertEqual(t.completed, True)
542
543         self.assertEqual(t._original_data['status'], "pending")
544         self.assertEqual(t._original_data['description'], "Buy some milk")
545         self.assertEqual(set(t._modified_fields),
546                          set(['status', 'description']))
547
548     def test_export_data(self):
549         t = Task(self.tw, description="test task",
550             project="Home",
551             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
552
553         # Check that the output is a permutation of:
554         # {"project":"Home","description":"test task","due":"20150101232323Z"}
555         allowed_segments = self.exported_raw_data[1:-1].split(',')
556         allowed_output = [
557             '{' + ','.join(segments) + '}'
558             for segments in itertools.permutations(allowed_segments)
559         ]
560
561         self.assertTrue(any(t.export_data() == expected
562                             for expected in allowed_output))
563
564 class TimezoneAwareDatetimeTest(TasklibTest):
565
566     def setUp(self):
567         super(TimezoneAwareDatetimeTest, self).setUp()
568         self.zone = local_zone
569         self.localdate_naive = datetime.datetime(2015,2,2)
570         self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
571         self.localtime_aware = self.zone.localize(self.localtime_naive)
572         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
573
574     def test_timezone_naive_datetime_setitem(self):
575         t = Task(self.tw, description="test task")
576         t['due'] = self.localtime_naive
577         self.assertEqual(t['due'], self.localtime_aware)
578
579     def test_timezone_naive_datetime_using_init(self):
580         t = Task(self.tw, description="test task", due=self.localtime_naive)
581         self.assertEqual(t['due'], self.localtime_aware)
582
583     def test_filter_by_naive_datetime(self):
584         t = Task(self.tw, description="task1", due=self.localtime_naive)
585         t.save()
586         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
587         self.assertEqual(len(matching_tasks), 1)
588
589     def test_serialize_naive_datetime(self):
590         t = Task(self.tw, description="task1", due=self.localtime_naive)
591         self.assertEqual(json.loads(t.export_data())['due'], 
592                          self.utctime_aware.strftime(DATE_FORMAT))
593
594     def test_timezone_naive_date_setitem(self):
595         t = Task(self.tw, description="test task")
596         t['due'] = self.localdate_naive
597         self.assertEqual(t['due'], self.localtime_aware)
598
599     def test_timezone_naive_date_using_init(self):
600         t = Task(self.tw, description="test task", due=self.localdate_naive)
601         self.assertEqual(t['due'], self.localtime_aware)
602
603     def test_filter_by_naive_date(self):
604         t = Task(self.tw, description="task1", due=self.localdate_naive)
605         t.save()
606         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
607         self.assertEqual(len(matching_tasks), 1)
608
609     def test_serialize_naive_date(self):
610         t = Task(self.tw, description="task1", due=self.localdate_naive)
611         self.assertEqual(json.loads(t.export_data())['due'], 
612                          self.utctime_aware.strftime(DATE_FORMAT))
613
614     def test_timezone_aware_datetime_setitem(self):
615         t = Task(self.tw, description="test task")
616         t['due'] = self.localtime_aware
617         self.assertEqual(t['due'], self.localtime_aware)
618
619     def test_timezone_aware_datetime_using_init(self):
620         t = Task(self.tw, description="test task", due=self.localtime_aware)
621         self.assertEqual(t['due'], self.localtime_aware)
622
623     def test_filter_by_aware_datetime(self):
624         t = Task(self.tw, description="task1", due=self.localtime_aware)
625         t.save()
626         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
627         self.assertEqual(len(matching_tasks), 1)
628
629     def test_serialize_aware_datetime(self):
630         t = Task(self.tw, description="task1", due=self.localtime_aware)
631         self.assertEqual(json.loads(t.export_data())['due'], 
632                          self.utctime_aware.strftime(DATE_FORMAT))
633
634 class AnnotationTest(TasklibTest):
635
636     def setUp(self):
637         super(AnnotationTest, self).setUp()
638         Task(self.tw, description="test task").save()
639
640     def test_adding_annotation(self):
641         task = self.tw.tasks.get()
642         task.add_annotation('test annotation')
643         self.assertEqual(len(task['annotations']), 1)
644         ann = task['annotations'][0]
645         self.assertEqual(ann['description'], 'test annotation')
646
647     def test_removing_annotation(self):
648         task = self.tw.tasks.get()
649         task.add_annotation('test annotation')
650         ann = task['annotations'][0]
651         ann.remove()
652         self.assertEqual(len(task['annotations']), 0)
653
654     def test_removing_annotation_by_description(self):
655         task = self.tw.tasks.get()
656         task.add_annotation('test annotation')
657         task.remove_annotation('test annotation')
658         self.assertEqual(len(task['annotations']), 0)
659
660     def test_removing_annotation_by_obj(self):
661         task = self.tw.tasks.get()
662         task.add_annotation('test annotation')
663         ann = task['annotations'][0]
664         task.remove_annotation(ann)
665         self.assertEqual(len(task['annotations']), 0)
666
667     def test_annotation_after_modification(self):
668          task = self.tw.tasks.get()
669          task['project'] = 'test'
670          task.add_annotation('I should really do this task')
671          self.assertEqual(task['project'], 'test')
672          task.save()
673          self.assertEqual(task['project'], 'test')
674
675     def test_serialize_annotations(self):
676         # Test that serializing annotations is possible
677         t = Task(self.tw, description="test")
678         t.save()
679
680         t.add_annotation("annotation1")
681         t.add_annotation("annotation2")
682
683         data = t._serialize('annotations', t._data['annotations'])
684
685         self.assertEqual(len(data), 2)
686         self.assertEqual(type(data[0]), dict)
687         self.assertEqual(type(data[1]), dict)
688
689         self.assertEqual(data[0]['description'], "annotation1")
690         self.assertEqual(data[1]['description'], "annotation2")
691
692
693 class UnicodeTest(TasklibTest):
694
695     def test_unicode_task(self):
696         Task(self.tw, description="†åßk").save()
697         self.tw.tasks.get()
698
699     def test_non_unicode_task(self):
700         Task(self.tw, description="test task").save()
701         self.tw.tasks.get()