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

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