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

f7b9a2724614ba2004fdd4b6a2b4c492956eb530
[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
507 class TaskFromHookTest(TasklibTest):
508
509     input_add_data = six.StringIO(
510         '{"description":"Buy some milk",'
511         '"entry":"20141118T050231Z",'
512         '"status":"pending",'
513         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
514
515     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
516         '{"description":"Buy some milk finally",'
517         '"entry":"20141118T050231Z",'
518         '"status":"completed",'
519         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
520
521     exported_raw_data = (
522         '{"project":"Home",'
523          '"due":"20150101T232323Z",'
524          '"description":"test task"}')
525
526     def test_setting_up_from_add_hook_input(self):
527         t = Task.from_input(input_file=self.input_add_data)
528         self.assertEqual(t['description'], "Buy some milk")
529         self.assertEqual(t.pending, True)
530
531     def test_setting_up_from_modified_hook_input(self):
532         t = Task.from_input(input_file=self.input_modify_data, modify=True)
533         self.assertEqual(t['description'], "Buy some milk finally")
534         self.assertEqual(t.pending, False)
535         self.assertEqual(t.completed, True)
536
537         self.assertEqual(t._original_data['status'], "pending")
538         self.assertEqual(t._original_data['description'], "Buy some milk")
539         self.assertEqual(set(t._modified_fields),
540                          set(['status', 'description']))
541
542     def test_export_data(self):
543         t = Task(self.tw, description="test task",
544             project="Home",
545             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
546
547         # Check that the output is a permutation of:
548         # {"project":"Home","description":"test task","due":"20150101232323Z"}
549         allowed_segments = self.exported_raw_data[1:-1].split(',')
550         allowed_output = [
551             '{' + ','.join(segments) + '}'
552             for segments in itertools.permutations(allowed_segments)
553         ]
554
555         self.assertTrue(any(t.export_data() == expected
556                             for expected in allowed_output))
557
558 class TimezoneAwareDatetimeTest(TasklibTest):
559
560     def setUp(self):
561         super(TimezoneAwareDatetimeTest, self).setUp()
562         self.zone = local_zone
563         self.localdate_naive = datetime.datetime(2015,2,2)
564         self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
565         self.localtime_aware = self.zone.localize(self.localtime_naive)
566         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
567
568     def test_timezone_naive_datetime_setitem(self):
569         t = Task(self.tw, description="test task")
570         t['due'] = self.localtime_naive
571         self.assertEqual(t['due'], self.localtime_aware)
572
573     def test_timezone_naive_datetime_using_init(self):
574         t = Task(self.tw, description="test task", due=self.localtime_naive)
575         self.assertEqual(t['due'], self.localtime_aware)
576
577     def test_filter_by_naive_datetime(self):
578         t = Task(self.tw, description="task1", due=self.localtime_naive)
579         t.save()
580         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
581         self.assertEqual(len(matching_tasks), 1)
582
583     def test_serialize_naive_datetime(self):
584         t = Task(self.tw, description="task1", due=self.localtime_naive)
585         self.assertEqual(json.loads(t.export_data())['due'], 
586                          self.utctime_aware.strftime(DATE_FORMAT))
587
588     def test_timezone_naive_date_setitem(self):
589         t = Task(self.tw, description="test task")
590         t['due'] = self.localdate_naive
591         self.assertEqual(t['due'], self.localtime_aware)
592
593     def test_timezone_naive_date_using_init(self):
594         t = Task(self.tw, description="test task", due=self.localdate_naive)
595         self.assertEqual(t['due'], self.localtime_aware)
596
597     def test_filter_by_naive_date(self):
598         t = Task(self.tw, description="task1", due=self.localdate_naive)
599         t.save()
600         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
601         self.assertEqual(len(matching_tasks), 1)
602
603     def test_serialize_naive_date(self):
604         t = Task(self.tw, description="task1", due=self.localdate_naive)
605         self.assertEqual(json.loads(t.export_data())['due'], 
606                          self.utctime_aware.strftime(DATE_FORMAT))
607
608     def test_timezone_aware_datetime_setitem(self):
609         t = Task(self.tw, description="test task")
610         t['due'] = self.localtime_aware
611         self.assertEqual(t['due'], self.localtime_aware)
612
613     def test_timezone_aware_datetime_using_init(self):
614         t = Task(self.tw, description="test task", due=self.localtime_aware)
615         self.assertEqual(t['due'], self.localtime_aware)
616
617     def test_filter_by_aware_datetime(self):
618         t = Task(self.tw, description="task1", due=self.localtime_aware)
619         t.save()
620         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
621         self.assertEqual(len(matching_tasks), 1)
622
623     def test_serialize_aware_datetime(self):
624         t = Task(self.tw, description="task1", due=self.localtime_aware)
625         self.assertEqual(json.loads(t.export_data())['due'], 
626                          self.utctime_aware.strftime(DATE_FORMAT))
627
628 class AnnotationTest(TasklibTest):
629
630     def setUp(self):
631         super(AnnotationTest, self).setUp()
632         Task(self.tw, description="test task").save()
633
634     def test_adding_annotation(self):
635         task = self.tw.tasks.get()
636         task.add_annotation('test annotation')
637         self.assertEqual(len(task['annotations']), 1)
638         ann = task['annotations'][0]
639         self.assertEqual(ann['description'], 'test annotation')
640
641     def test_removing_annotation(self):
642         task = self.tw.tasks.get()
643         task.add_annotation('test annotation')
644         ann = task['annotations'][0]
645         ann.remove()
646         self.assertEqual(len(task['annotations']), 0)
647
648     def test_removing_annotation_by_description(self):
649         task = self.tw.tasks.get()
650         task.add_annotation('test annotation')
651         task.remove_annotation('test annotation')
652         self.assertEqual(len(task['annotations']), 0)
653
654     def test_removing_annotation_by_obj(self):
655         task = self.tw.tasks.get()
656         task.add_annotation('test annotation')
657         ann = task['annotations'][0]
658         task.remove_annotation(ann)
659         self.assertEqual(len(task['annotations']), 0)
660
661     def test_annotation_after_modification(self):
662          task = self.tw.tasks.get()
663          task['project'] = 'test'
664          task.add_annotation('I should really do this task')
665          self.assertEqual(task['project'], 'test')
666          task.save()
667          self.assertEqual(task['project'], 'test')
668
669     def test_serialize_annotations(self):
670         # Test that serializing annotations is possible
671         t = Task(self.tw, description="test")
672         t.save()
673
674         t.add_annotation("annotation1")
675         t.add_annotation("annotation2")
676
677         data = t._serialize('annotations', t._data['annotations'])
678
679         self.assertEqual(len(data), 2)
680         self.assertEqual(type(data[0]), dict)
681         self.assertEqual(type(data[1]), dict)
682
683         self.assertEqual(data[0]['description'], "annotation1")
684         self.assertEqual(data[1]['description'], "annotation2")
685
686
687 class UnicodeTest(TasklibTest):
688
689     def test_unicode_task(self):
690         Task(self.tw, description="†åßk").save()
691         self.tw.tasks.get()
692
693     def test_non_unicode_task(self):
694         Task(self.tw, description="test task").save()
695         self.tw.tasks.get()