]> 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: Add test for modifiying a large number of tasks at once
[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, taskrc_location='/')
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     def test_filter_dummy_by_status(self):
133         t = Task(self.tw, description="test")
134         t.save()
135
136         tasks = self.tw.tasks.filter(status=t['status'])
137         self.assertEqual(list(tasks), [t])
138
139     def test_filter_dummy_by_uuid(self):
140         t = Task(self.tw, description="test")
141         t.save()
142
143         tasks = self.tw.tasks.filter(uuid=t['uuid'])
144         self.assertEqual(list(tasks), [t])
145
146     def test_filter_dummy_by_entry(self):
147         t = Task(self.tw, description="test")
148         t.save()
149
150         tasks = self.tw.tasks.filter(entry=t['entry'])
151         self.assertEqual(list(tasks), [t])
152
153     def test_filter_dummy_by_description(self):
154         t = Task(self.tw, description="test")
155         t.save()
156
157         tasks = self.tw.tasks.filter(description=t['description'])
158         self.assertEqual(list(tasks), [t])
159
160     def test_filter_dummy_by_start(self):
161         t = Task(self.tw, description="test")
162         t.save()
163         t.start()
164
165         tasks = self.tw.tasks.filter(start=t['start'])
166         self.assertEqual(list(tasks), [t])
167
168     def test_filter_dummy_by_end(self):
169         t = Task(self.tw, description="test")
170         t.save()
171         t.done()
172
173         tasks = self.tw.tasks.filter(end=t['end'])
174         self.assertEqual(list(tasks), [t])
175
176     def test_filter_dummy_by_due(self):
177         t = Task(self.tw, description="test", due=datetime.datetime.now())
178         t.save()
179
180         tasks = self.tw.tasks.filter(due=t['due'])
181         self.assertEqual(list(tasks), [t])
182
183     def test_filter_dummy_by_until(self):
184         t = Task(self.tw, description="test")
185         t.save()
186
187         tasks = self.tw.tasks.filter(until=t['until'])
188         self.assertEqual(list(tasks), [t])
189
190     def test_filter_dummy_by_modified(self):
191         # Older TW version does not support bumping modified
192         # on save
193         if self.tw.version < six.text_type('2.2.0'):
194             # Python2.6 does not support SkipTest. As a workaround
195             # mark the test as passed by exiting.
196             if getattr(unittest, 'SkipTest', None) is not None:
197                 raise unittest.SkipTest()
198             else:
199                 return
200
201         t = Task(self.tw, description="test")
202         t.save()
203
204         tasks = self.tw.tasks.filter(modified=t['modified'])
205         self.assertEqual(list(tasks), [t])
206
207     def test_filter_dummy_by_scheduled(self):
208         t = Task(self.tw, description="test")
209         t.save()
210
211         tasks = self.tw.tasks.filter(scheduled=t['scheduled'])
212         self.assertEqual(list(tasks), [t])
213
214     def test_filter_dummy_by_tags(self):
215         t = Task(self.tw, description="test", tags=["home"])
216         t.save()
217
218         tasks = self.tw.tasks.filter(tags=t['tags'])
219         self.assertEqual(list(tasks), [t])
220
221     def test_filter_dummy_by_projects(self):
222         t = Task(self.tw, description="test", project="random")
223         t.save()
224
225         tasks = self.tw.tasks.filter(project=t['project'])
226         self.assertEqual(list(tasks), [t])
227
228     def test_filter_by_priority(self):
229         t = Task(self.tw, description="test", priority="H")
230         t.save()
231
232         tasks = self.tw.tasks.filter(priority=t['priority'])
233         self.assertEqual(list(tasks), [t])
234
235
236 class TaskTest(TasklibTest):
237
238     def test_create_unsaved_task(self):
239         # Make sure a new task is not saved unless explicitly called for
240         t = Task(self.tw, description="test task")
241         self.assertEqual(len(self.tw.tasks.all()), 0)
242
243     # TODO: once python 2.6 compatiblity is over, use context managers here
244     #       and in all subsequent tests for assertRaises
245
246     def test_delete_unsaved_task(self):
247         t = Task(self.tw, description="test task")
248         self.assertRaises(Task.NotSaved, t.delete)
249
250     def test_complete_unsaved_task(self):
251         t = Task(self.tw, description="test task")
252         self.assertRaises(Task.NotSaved, t.done)
253
254     def test_refresh_unsaved_task(self):
255         t = Task(self.tw, description="test task")
256         self.assertRaises(Task.NotSaved, t.refresh)
257
258     def test_start_unsaved_task(self):
259         t = Task(self.tw, description="test task")
260         self.assertRaises(Task.NotSaved, t.start)
261
262     def test_delete_deleted_task(self):
263         t = Task(self.tw, description="test task")
264         t.save()
265         t.delete()
266
267         self.assertRaises(Task.DeletedTask, t.delete)
268
269     def test_complete_completed_task(self):
270         t = Task(self.tw, description="test task")
271         t.save()
272         t.done()
273
274         self.assertRaises(Task.CompletedTask, t.done)
275
276     def test_start_completed_task(self):
277         t = Task(self.tw, description="test task")
278         t.save()
279         t.done()
280
281         self.assertRaises(Task.CompletedTask, t.start)
282
283     def test_complete_deleted_task(self):
284         t = Task(self.tw, description="test task")
285         t.save()
286         t.delete()
287
288         self.assertRaises(Task.DeletedTask, t.done)
289
290     def test_start_completed_task(self):
291         t = Task(self.tw, description="test task")
292         t.save()
293         t.done()
294
295         self.assertRaises(Task.CompletedTask, t.start)
296
297     def test_starting_task(self):
298         t = Task(self.tw, description="test task")
299         now = t.datetime_normalizer(datetime.datetime.now())
300         t.save()
301         t.start()
302
303         self.assertTrue(now.replace(microsecond=0) <= t['start'])
304         self.assertEqual(t['status'], 'pending')
305
306     def test_completing_task(self):
307         t = Task(self.tw, description="test task")
308         now = t.datetime_normalizer(datetime.datetime.now())
309         t.save()
310         t.done()
311
312         self.assertTrue(now.replace(microsecond=0) <= t['end'])
313         self.assertEqual(t['status'], 'completed')
314
315     def test_deleting_task(self):
316         t = Task(self.tw, description="test task")
317         now = t.datetime_normalizer(datetime.datetime.now())
318         t.save()
319         t.delete()
320
321         self.assertTrue(now.replace(microsecond=0) <= t['end'])
322         self.assertEqual(t['status'], 'deleted')
323
324     def test_modify_simple_attribute_without_space(self):
325         t = Task(self.tw, description="test")
326         t.save()
327
328         self.assertEquals(t['description'], "test")
329
330         t['description'] = "test-modified"
331         t.save()
332
333         self.assertEquals(t['description'], "test-modified")
334
335     def test_modify_simple_attribute_with_space(self):
336         # Space can pose problems with parsing
337         t = Task(self.tw, description="test task")
338         t.save()
339
340         self.assertEquals(t['description'], "test task")
341
342         t['description'] = "test task modified"
343         t.save()
344
345         self.assertEquals(t['description'], "test task modified")
346
347     def test_empty_dependency_set_of_unsaved_task(self):
348         t = Task(self.tw, description="test task")
349         self.assertEqual(t['depends'], set())
350
351     def test_empty_dependency_set_of_saved_task(self):
352         t = Task(self.tw, description="test task")
353         t.save()
354         self.assertEqual(t['depends'], set())
355
356     def test_set_unsaved_task_as_dependency(self):
357         # Adds only one dependency to task with no dependencies
358         t = Task(self.tw, description="test task")
359         dependency = Task(self.tw, description="needs to be done first")
360
361         # We only save the parent task, dependency task is unsaved
362         t.save()
363         t['depends'] = set([dependency])
364
365         self.assertRaises(Task.NotSaved, t.save)
366
367     def test_set_simple_dependency_set(self):
368         # Adds only one dependency to task with no dependencies
369         t = Task(self.tw, description="test task")
370         dependency = Task(self.tw, description="needs to be done first")
371
372         t.save()
373         dependency.save()
374
375         t['depends'] = set([dependency])
376
377         self.assertEqual(t['depends'], set([dependency]))
378
379     def test_set_complex_dependency_set(self):
380         # Adds two dependencies to task with no dependencies
381         t = Task(self.tw, description="test task")
382         dependency1 = Task(self.tw, description="needs to be done first")
383         dependency2 = Task(self.tw, description="needs to be done second")
384
385         t.save()
386         dependency1.save()
387         dependency2.save()
388
389         t['depends'] = set([dependency1, dependency2])
390
391         self.assertEqual(t['depends'], set([dependency1, dependency2]))
392
393     def test_remove_from_dependency_set(self):
394         # Removes dependency from task with two dependencies
395         t = Task(self.tw, description="test task")
396         dependency1 = Task(self.tw, description="needs to be done first")
397         dependency2 = Task(self.tw, description="needs to be done second")
398
399         dependency1.save()
400         dependency2.save()
401
402         t['depends'] = set([dependency1, dependency2])
403         t.save()
404
405         t['depends'].remove(dependency2)
406         t.save()
407
408         self.assertEqual(t['depends'], set([dependency1]))
409
410     def test_add_to_dependency_set(self):
411         # Adds dependency to task with one dependencies
412         t = Task(self.tw, description="test task")
413         dependency1 = Task(self.tw, description="needs to be done first")
414         dependency2 = Task(self.tw, description="needs to be done second")
415
416         dependency1.save()
417         dependency2.save()
418
419         t['depends'] = set([dependency1])
420         t.save()
421
422         t['depends'].add(dependency2)
423         t.save()
424
425         self.assertEqual(t['depends'], set([dependency1, dependency2]))
426
427     def test_add_to_empty_dependency_set(self):
428         # Adds dependency to task with one dependencies
429         t = Task(self.tw, description="test task")
430         dependency = Task(self.tw, description="needs to be done first")
431
432         dependency.save()
433
434         t['depends'].add(dependency)
435         t.save()
436
437         self.assertEqual(t['depends'], set([dependency]))
438
439     def test_simple_dependency_set_save_repeatedly(self):
440         # Adds only one dependency to task with no dependencies
441         t = Task(self.tw, description="test task")
442         dependency = Task(self.tw, description="needs to be done first")
443         dependency.save()
444
445         t['depends'] = set([dependency])
446         t.save()
447
448         # We taint the task, but keep depends intact
449         t['description'] = "test task modified"
450         t.save()
451
452         self.assertEqual(t['depends'], set([dependency]))
453
454         # We taint the task, but assign the same set to the depends
455         t['depends'] = set([dependency])
456         t['description'] = "test task modified again"
457         t.save()
458
459         self.assertEqual(t['depends'], set([dependency]))
460
461     def test_compare_different_tasks(self):
462         # Negative: compare two different tasks
463         t1 = Task(self.tw, description="test task")
464         t2 = Task(self.tw, description="test task")
465
466         t1.save()
467         t2.save()
468
469         self.assertEqual(t1 == t2, False)
470
471     def test_compare_same_task_object(self):
472         # Compare Task object wit itself
473         t = Task(self.tw, description="test task")
474         t.save()
475
476         self.assertEqual(t == t, True)
477
478     def test_compare_same_task(self):
479         # Compare the same task using two different objects
480         t1 = Task(self.tw, description="test task")
481         t1.save()
482
483         t2 = self.tw.tasks.get(uuid=t1['uuid'])
484         self.assertEqual(t1 == t2, True)
485
486     def test_compare_unsaved_tasks(self):
487         # t1 and t2 are unsaved tasks, considered to be unequal
488         # despite the content of data
489         t1 = Task(self.tw, description="test task")
490         t2 = Task(self.tw, description="test task")
491
492         self.assertEqual(t1 == t2, False)
493
494     def test_hash_unsaved_tasks(self):
495         # Considered equal, it's the same object
496         t1 = Task(self.tw, description="test task")
497         t2 = t1
498         self.assertEqual(hash(t1) == hash(t2), True)
499
500     def test_hash_same_task(self):
501         # Compare the hash of the task using two different objects
502         t1 = Task(self.tw, description="test task")
503         t1.save()
504
505         t2 = self.tw.tasks.get(uuid=t1['uuid'])
506         self.assertEqual(t1.__hash__(), t2.__hash__())
507
508     def test_adding_task_with_priority(self):
509         t = Task(self.tw, description="test task", priority="M")
510         t.save()
511
512     def test_removing_priority_with_none(self):
513         t = Task(self.tw, description="test task", priority="L")
514         t.save()
515
516         # Remove the priority mark
517         t['priority'] = None
518         t.save()
519
520         # Assert that priority is not there after saving
521         self.assertEqual(t['priority'], None)
522
523     def test_adding_task_with_due_time(self):
524         t = Task(self.tw, description="test task", due=datetime.datetime.now())
525         t.save()
526
527     def test_removing_due_time_with_none(self):
528         t = Task(self.tw, description="test task", due=datetime.datetime.now())
529         t.save()
530
531         # Remove the due timestamp
532         t['due'] = None
533         t.save()
534
535         # Assert that due timestamp is no longer there
536         self.assertEqual(t['due'], None)
537
538     def test_modified_fields_new_task(self):
539         t = Task(self.tw)
540
541         # This should be empty with new task
542         self.assertEqual(set(t._modified_fields), set())
543
544         # Modify the task
545         t['description'] = "test task"
546         self.assertEqual(set(t._modified_fields), set(['description']))
547
548         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
549         self.assertEqual(set(t._modified_fields), set(['description', 'due']))
550
551         t['project'] = "test project"
552         self.assertEqual(set(t._modified_fields),
553                          set(['description', 'due', 'project']))
554
555         # List of modified fields should clear out when saved
556         t.save()
557         self.assertEqual(set(t._modified_fields), set())
558
559         # Reassigning the fields with the same values now should not produce
560         # modified fields
561         t['description'] = "test task"
562         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
563         t['project'] = "test project"
564         self.assertEqual(set(t._modified_fields), set())
565
566     def test_modified_fields_loaded_task(self):
567         t = Task(self.tw)
568
569         # Modify the task
570         t['description'] = "test task"
571         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
572         t['project'] = "test project"
573
574         dependency = Task(self.tw, description="dependency")
575         dependency.save()
576         t['depends'] = set([dependency])
577
578         # List of modified fields should clear out when saved
579         t.save()
580         self.assertEqual(set(t._modified_fields), set())
581
582         # Get the task by using a filter by UUID
583         t2 = self.tw.tasks.get(uuid=t['uuid'])
584
585         # Reassigning the fields with the same values now should not produce
586         # modified fields
587         t['description'] = "test task"
588         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
589         t['project'] = "test project"
590         t['depends'] = set([dependency])
591         self.assertEqual(set(t._modified_fields), set())
592
593     def test_modified_fields_not_affected_by_reading(self):
594         t = Task(self.tw)
595
596         for field in TASK_STANDARD_ATTRS:
597             value = t[field]
598
599         self.assertEqual(set(t._modified_fields), set())
600
601     def test_setting_read_only_attrs_through_init(self):
602         # Test that we are unable to set readonly attrs through __init__
603         for readonly_key in Task.read_only_fields:
604             kwargs = {'description': 'test task', readonly_key: 'value'}
605             self.assertRaises(RuntimeError,
606                               lambda: Task(self.tw, **kwargs))
607
608     def test_setting_read_only_attrs_through_setitem(self):
609         # Test that we are unable to set readonly attrs through __init__
610         for readonly_key in Task.read_only_fields:
611             t = Task(self.tw, description='test task')
612             self.assertRaises(RuntimeError,
613                               lambda: t.__setitem__(readonly_key, 'value'))
614
615     def test_saving_unmodified_task(self):
616         t = Task(self.tw, description="test task")
617         t.save()
618         t.save()
619
620     def test_adding_tag_by_appending(self):
621         t = Task(self.tw, description="test task", tags=['test1'])
622         t.save()
623         t['tags'].append('test2')
624         t.save()
625         self.assertEqual(t['tags'], ['test1', 'test2'])
626
627     def test_adding_tag_by_appending_empty(self):
628         t = Task(self.tw, description="test task")
629         t.save()
630         t['tags'].append('test')
631         t.save()
632         self.assertEqual(t['tags'], ['test'])
633
634     def test_serializers_returning_empty_string_for_none(self):
635         # Test that any serializer returns '' when passed None
636         t = Task(self.tw)
637         serializers = [getattr(t, serializer_name) for serializer_name in
638                        filter(lambda x: x.startswith('serialize_'), dir(t))]
639         for serializer in serializers:
640             self.assertEqual(serializer(None), '')
641
642     def test_deserializer_returning_empty_value_for_empty_string(self):
643         # Test that any deserializer returns empty value when passed ''
644         t = Task(self.tw)
645         deserializers = [getattr(t, deserializer_name) for deserializer_name in
646                         filter(lambda x: x.startswith('deserialize_'), dir(t))]
647         for deserializer in deserializers:
648             self.assertTrue(deserializer('') in (None, [], set()))
649
650     def test_normalizers_handling_none(self):
651         # Test that any normalizer can handle None as a valid value
652         t = Task(self.tw)
653
654         for key in TASK_STANDARD_ATTRS:
655             t._normalize(key, None)
656
657     def test_recurrent_task_generation(self):
658         today = datetime.date.today()
659         t = Task(self.tw, description="brush teeth",
660                  due=today, recur="daily")
661         t.save()
662         self.assertEqual(len(self.tw.tasks.pending()), 2)
663
664     def test_modify_number_of_tasks_at_once(self):
665         for i in range(1, 100):
666             Task(self.tw, description="test task %d" % i, tags=['test']).save()
667
668         self.tw.execute_command(['+test', 'mod', 'unified', 'description'])
669
670
671 class TaskFromHookTest(TasklibTest):
672
673     input_add_data = six.StringIO(
674         '{"description":"Buy some milk",'
675         '"entry":"20141118T050231Z",'
676         '"status":"pending",'
677         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
678
679     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
680         '{"description":"Buy some milk finally",'
681         '"entry":"20141118T050231Z",'
682         '"status":"completed",'
683         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
684
685     exported_raw_data = (
686         '{"project":"Home",'
687          '"due":"20150101T232323Z",'
688          '"description":"test task"}')
689
690     def test_setting_up_from_add_hook_input(self):
691         t = Task.from_input(input_file=self.input_add_data, warrior=self.tw)
692         self.assertEqual(t['description'], "Buy some milk")
693         self.assertEqual(t.pending, True)
694
695     def test_setting_up_from_modified_hook_input(self):
696         t = Task.from_input(input_file=self.input_modify_data, modify=True,
697                             warrior=self.tw)
698         self.assertEqual(t['description'], "Buy some milk finally")
699         self.assertEqual(t.pending, False)
700         self.assertEqual(t.completed, True)
701
702         self.assertEqual(t._original_data['status'], "pending")
703         self.assertEqual(t._original_data['description'], "Buy some milk")
704         self.assertEqual(set(t._modified_fields),
705                          set(['status', 'description']))
706
707     def test_export_data(self):
708         t = Task(self.tw, description="test task",
709             project="Home",
710             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
711
712         # Check that the output is a permutation of:
713         # {"project":"Home","description":"test task","due":"20150101232323Z"}
714         allowed_segments = self.exported_raw_data[1:-1].split(',')
715         allowed_output = [
716             '{' + ','.join(segments) + '}'
717             for segments in itertools.permutations(allowed_segments)
718         ]
719
720         self.assertTrue(any(t.export_data() == expected
721                             for expected in allowed_output))
722
723 class TimezoneAwareDatetimeTest(TasklibTest):
724
725     def setUp(self):
726         super(TimezoneAwareDatetimeTest, self).setUp()
727         self.zone = local_zone
728         self.localdate_naive = datetime.datetime(2015,2,2)
729         self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
730         self.localtime_aware = self.zone.localize(self.localtime_naive)
731         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
732
733     def test_timezone_naive_datetime_setitem(self):
734         t = Task(self.tw, description="test task")
735         t['due'] = self.localtime_naive
736         self.assertEqual(t['due'], self.localtime_aware)
737
738     def test_timezone_naive_datetime_using_init(self):
739         t = Task(self.tw, description="test task", due=self.localtime_naive)
740         self.assertEqual(t['due'], self.localtime_aware)
741
742     def test_filter_by_naive_datetime(self):
743         t = Task(self.tw, description="task1", due=self.localtime_naive)
744         t.save()
745         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
746         self.assertEqual(len(matching_tasks), 1)
747
748     def test_serialize_naive_datetime(self):
749         t = Task(self.tw, description="task1", due=self.localtime_naive)
750         self.assertEqual(json.loads(t.export_data())['due'],
751                          self.utctime_aware.strftime(DATE_FORMAT))
752
753     def test_timezone_naive_date_setitem(self):
754         t = Task(self.tw, description="test task")
755         t['due'] = self.localdate_naive
756         self.assertEqual(t['due'], self.localtime_aware)
757
758     def test_timezone_naive_date_using_init(self):
759         t = Task(self.tw, description="test task", due=self.localdate_naive)
760         self.assertEqual(t['due'], self.localtime_aware)
761
762     def test_filter_by_naive_date(self):
763         t = Task(self.tw, description="task1", due=self.localdate_naive)
764         t.save()
765         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
766         self.assertEqual(len(matching_tasks), 1)
767
768     def test_serialize_naive_date(self):
769         t = Task(self.tw, description="task1", due=self.localdate_naive)
770         self.assertEqual(json.loads(t.export_data())['due'],
771                          self.utctime_aware.strftime(DATE_FORMAT))
772
773     def test_timezone_aware_datetime_setitem(self):
774         t = Task(self.tw, description="test task")
775         t['due'] = self.localtime_aware
776         self.assertEqual(t['due'], self.localtime_aware)
777
778     def test_timezone_aware_datetime_using_init(self):
779         t = Task(self.tw, description="test task", due=self.localtime_aware)
780         self.assertEqual(t['due'], self.localtime_aware)
781
782     def test_filter_by_aware_datetime(self):
783         t = Task(self.tw, description="task1", due=self.localtime_aware)
784         t.save()
785         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
786         self.assertEqual(len(matching_tasks), 1)
787
788     def test_serialize_aware_datetime(self):
789         t = Task(self.tw, description="task1", due=self.localtime_aware)
790         self.assertEqual(json.loads(t.export_data())['due'],
791                          self.utctime_aware.strftime(DATE_FORMAT))
792
793 class DatetimeStringTest(TasklibTest):
794
795     def test_simple_now_conversion(self):
796         if self.tw.version < six.text_type('2.4.0'):
797             # Python2.6 does not support SkipTest. As a workaround
798             # mark the test as passed by exiting.
799             if getattr(unittest, 'SkipTest', None) is not None:
800                 raise unittest.SkipTest()
801             else:
802                 return
803
804         t = Task(self.tw, description="test task", due="now")
805         now = local_zone.localize(datetime.datetime.now())
806
807         # Assert that both times are not more than 5 seconds apart
808         self.assertTrue((now - t['due']).total_seconds() < 5)
809         self.assertTrue((t['due'] - now).total_seconds() < 5)
810
811     def test_simple_eoy_conversion(self):
812         if self.tw.version < six.text_type('2.4.0'):
813             # Python2.6 does not support SkipTest. As a workaround
814             # mark the test as passed by exiting.
815             if getattr(unittest, 'SkipTest', None) is not None:
816                 raise unittest.SkipTest()
817             else:
818                 return
819
820         t = Task(self.tw, description="test task", due="eoy")
821         now = local_zone.localize(datetime.datetime.now())
822         eoy = local_zone.localize(datetime.datetime(
823             year=now.year,
824             month=12,
825             day=31,
826             hour=23,
827             minute=59,
828             second=59
829             ))
830         self.assertEqual(eoy, t['due'])
831
832     def test_complex_eoy_conversion(self):
833         if self.tw.version < six.text_type('2.4.0'):
834             # Python2.6 does not support SkipTest. As a workaround
835             # mark the test as passed by exiting.
836             if getattr(unittest, 'SkipTest', None) is not None:
837                 raise unittest.SkipTest()
838             else:
839                 return
840
841         t = Task(self.tw, description="test task", due="eoy - 4 months")
842         now = local_zone.localize(datetime.datetime.now())
843         due_date = local_zone.localize(datetime.datetime(
844             year=now.year,
845             month=9,
846             day=3,
847             hour=0,
848             minute=59,
849             second=59
850             ))
851         self.assertEqual(due_date, t['due'])
852
853     def test_filtering_with_string_datetime(self):
854         t = Task(self.tw, description="test task",
855                  due=datetime.datetime.now() - datetime.timedelta(0,2))
856         t.save()
857         self.assertEqual(len(self.tw.tasks.filter(due__before="now")), 1)
858
859 class AnnotationTest(TasklibTest):
860
861     def setUp(self):
862         super(AnnotationTest, self).setUp()
863         Task(self.tw, description="test task").save()
864
865     def test_adding_annotation(self):
866         task = self.tw.tasks.get()
867         task.add_annotation('test annotation')
868         self.assertEqual(len(task['annotations']), 1)
869         ann = task['annotations'][0]
870         self.assertEqual(ann['description'], 'test annotation')
871
872     def test_removing_annotation(self):
873         task = self.tw.tasks.get()
874         task.add_annotation('test annotation')
875         ann = task['annotations'][0]
876         ann.remove()
877         self.assertEqual(len(task['annotations']), 0)
878
879     def test_removing_annotation_by_description(self):
880         task = self.tw.tasks.get()
881         task.add_annotation('test annotation')
882         task.remove_annotation('test annotation')
883         self.assertEqual(len(task['annotations']), 0)
884
885     def test_removing_annotation_by_obj(self):
886         task = self.tw.tasks.get()
887         task.add_annotation('test annotation')
888         ann = task['annotations'][0]
889         task.remove_annotation(ann)
890         self.assertEqual(len(task['annotations']), 0)
891
892     def test_annotation_after_modification(self):
893          task = self.tw.tasks.get()
894          task['project'] = 'test'
895          task.add_annotation('I should really do this task')
896          self.assertEqual(task['project'], 'test')
897          task.save()
898          self.assertEqual(task['project'], 'test')
899
900     def test_serialize_annotations(self):
901         # Test that serializing annotations is possible
902         t = Task(self.tw, description="test")
903         t.save()
904
905         t.add_annotation("annotation1")
906         t.add_annotation("annotation2")
907
908         data = t._serialize('annotations', t._data['annotations'])
909
910         self.assertEqual(len(data), 2)
911         self.assertEqual(type(data[0]), dict)
912         self.assertEqual(type(data[1]), dict)
913
914         self.assertEqual(data[0]['description'], "annotation1")
915         self.assertEqual(data[1]['description'], "annotation2")
916
917
918 class UnicodeTest(TasklibTest):
919
920     def test_unicode_task(self):
921         Task(self.tw, description="†åßk").save()
922         self.tw.tasks.get()
923
924     def test_non_unicode_task(self):
925         Task(self.tw, description="test task").save()
926         self.tw.tasks.get()
927
928 class ReadOnlyDictViewTest(unittest.TestCase):
929
930     def setUp(self):
931         self.sample = dict(l=[1,2,3], d={'k':'v'})
932         self.original_sample = copy.deepcopy(self.sample)
933         self.view = ReadOnlyDictView(self.sample)
934
935     def test_readonlydictview_getitem(self):
936         l = self.view['l']
937         self.assertEqual(l, self.sample['l'])
938
939         # Assert that modification changed only copied value
940         l.append(4)
941         self.assertNotEqual(l, self.sample['l'])
942
943         # Assert that viewed dict is not changed
944         self.assertEqual(self.sample, self.original_sample)
945
946     def test_readonlydictview_contains(self):
947         self.assertEqual('l' in self.view, 'l' in self.sample)
948         self.assertEqual('d' in self.view, 'd' in self.sample)
949         self.assertEqual('k' in self.view, 'k' in self.sample)
950
951         # Assert that viewed dict is not changed
952         self.assertEqual(self.sample, self.original_sample)
953
954     def test_readonlydictview_iter(self):
955         self.assertEqual(list(k for k in self.view),
956                          list(k for k in self.sample))
957
958         # Assert the view is correct after modification
959         self.sample['new'] = 'value'
960         self.assertEqual(list(k for k in self.view),
961                          list(k for k in self.sample))
962
963     def test_readonlydictview_len(self):
964         self.assertEqual(len(self.view), len(self.sample))
965
966         # Assert the view is correct after modification
967         self.sample['new'] = 'value'
968         self.assertEqual(len(self.view), len(self.sample))
969
970     def test_readonlydictview_get(self):
971         l = self.view.get('l')
972         self.assertEqual(l, self.sample.get('l'))
973
974         # Assert that modification changed only copied value
975         l.append(4)
976         self.assertNotEqual(l, self.sample.get('l'))
977
978         # Assert that viewed dict is not changed
979         self.assertEqual(self.sample, self.original_sample)
980
981     def test_readonlydict_items(self):
982         view_items = self.view.items()
983         sample_items = list(self.sample.items())
984         self.assertEqual(view_items, sample_items)
985
986         view_items.append('newkey')
987         self.assertNotEqual(view_items, sample_items)
988         self.assertEqual(self.sample, self.original_sample)
989
990     def test_readonlydict_values(self):
991         view_values = self.view.values()
992         sample_values = list(self.sample.values())
993         self.assertEqual(view_values, sample_values)
994
995         view_list_item = list(filter(lambda x: type(x) is list,
996                                      view_values))[0]
997         view_list_item.append(4)
998         self.assertNotEqual(view_values, sample_values)
999         self.assertEqual(self.sample, self.original_sample)