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

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