]> 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 tests for using TW-formatted strings as localized datetime values
[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 class TaskFromHookTest(TasklibTest):
665
666     input_add_data = six.StringIO(
667         '{"description":"Buy some milk",'
668         '"entry":"20141118T050231Z",'
669         '"status":"pending",'
670         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
671
672     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
673         '{"description":"Buy some milk finally",'
674         '"entry":"20141118T050231Z",'
675         '"status":"completed",'
676         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
677
678     exported_raw_data = (
679         '{"project":"Home",'
680          '"due":"20150101T232323Z",'
681          '"description":"test task"}')
682
683     def test_setting_up_from_add_hook_input(self):
684         t = Task.from_input(input_file=self.input_add_data, warrior=self.tw)
685         self.assertEqual(t['description'], "Buy some milk")
686         self.assertEqual(t.pending, True)
687
688     def test_setting_up_from_modified_hook_input(self):
689         t = Task.from_input(input_file=self.input_modify_data, modify=True,
690                             warrior=self.tw)
691         self.assertEqual(t['description'], "Buy some milk finally")
692         self.assertEqual(t.pending, False)
693         self.assertEqual(t.completed, True)
694
695         self.assertEqual(t._original_data['status'], "pending")
696         self.assertEqual(t._original_data['description'], "Buy some milk")
697         self.assertEqual(set(t._modified_fields),
698                          set(['status', 'description']))
699
700     def test_export_data(self):
701         t = Task(self.tw, description="test task",
702             project="Home",
703             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
704
705         # Check that the output is a permutation of:
706         # {"project":"Home","description":"test task","due":"20150101232323Z"}
707         allowed_segments = self.exported_raw_data[1:-1].split(',')
708         allowed_output = [
709             '{' + ','.join(segments) + '}'
710             for segments in itertools.permutations(allowed_segments)
711         ]
712
713         self.assertTrue(any(t.export_data() == expected
714                             for expected in allowed_output))
715
716 class TimezoneAwareDatetimeTest(TasklibTest):
717
718     def setUp(self):
719         super(TimezoneAwareDatetimeTest, self).setUp()
720         self.zone = local_zone
721         self.localdate_naive = datetime.datetime(2015,2,2)
722         self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
723         self.localtime_aware = self.zone.localize(self.localtime_naive)
724         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
725
726     def test_timezone_naive_datetime_setitem(self):
727         t = Task(self.tw, description="test task")
728         t['due'] = self.localtime_naive
729         self.assertEqual(t['due'], self.localtime_aware)
730
731     def test_timezone_naive_datetime_using_init(self):
732         t = Task(self.tw, description="test task", due=self.localtime_naive)
733         self.assertEqual(t['due'], self.localtime_aware)
734
735     def test_filter_by_naive_datetime(self):
736         t = Task(self.tw, description="task1", due=self.localtime_naive)
737         t.save()
738         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
739         self.assertEqual(len(matching_tasks), 1)
740
741     def test_serialize_naive_datetime(self):
742         t = Task(self.tw, description="task1", due=self.localtime_naive)
743         self.assertEqual(json.loads(t.export_data())['due'],
744                          self.utctime_aware.strftime(DATE_FORMAT))
745
746     def test_timezone_naive_date_setitem(self):
747         t = Task(self.tw, description="test task")
748         t['due'] = self.localdate_naive
749         self.assertEqual(t['due'], self.localtime_aware)
750
751     def test_timezone_naive_date_using_init(self):
752         t = Task(self.tw, description="test task", due=self.localdate_naive)
753         self.assertEqual(t['due'], self.localtime_aware)
754
755     def test_filter_by_naive_date(self):
756         t = Task(self.tw, description="task1", due=self.localdate_naive)
757         t.save()
758         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
759         self.assertEqual(len(matching_tasks), 1)
760
761     def test_serialize_naive_date(self):
762         t = Task(self.tw, description="task1", due=self.localdate_naive)
763         self.assertEqual(json.loads(t.export_data())['due'],
764                          self.utctime_aware.strftime(DATE_FORMAT))
765
766     def test_timezone_aware_datetime_setitem(self):
767         t = Task(self.tw, description="test task")
768         t['due'] = self.localtime_aware
769         self.assertEqual(t['due'], self.localtime_aware)
770
771     def test_timezone_aware_datetime_using_init(self):
772         t = Task(self.tw, description="test task", due=self.localtime_aware)
773         self.assertEqual(t['due'], self.localtime_aware)
774
775     def test_filter_by_aware_datetime(self):
776         t = Task(self.tw, description="task1", due=self.localtime_aware)
777         t.save()
778         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
779         self.assertEqual(len(matching_tasks), 1)
780
781     def test_serialize_aware_datetime(self):
782         t = Task(self.tw, description="task1", due=self.localtime_aware)
783         self.assertEqual(json.loads(t.export_data())['due'],
784                          self.utctime_aware.strftime(DATE_FORMAT))
785
786 class DatetimeStringTest(TasklibTest):
787
788     def test_simple_now_conversion(self):
789         if self.tw.version < six.text_type('2.4.0'):
790             # Python2.6 does not support SkipTest. As a workaround
791             # mark the test as passed by exiting.
792             if getattr(unittest, 'SkipTest', None) is not None:
793                 raise unittest.SkipTest()
794             else:
795                 return
796
797         t = Task(self.tw, description="test task", due="now")
798         now = local_zone.localize(datetime.datetime.now())
799
800         # Assert that both times are not more than 5 seconds apart
801         self.assertTrue((now - t['due']).total_seconds() < 5)
802         self.assertTrue((t['due'] - now).total_seconds() < 5)
803
804     def test_simple_eoy_conversion(self):
805         if self.tw.version < six.text_type('2.4.0'):
806             # Python2.6 does not support SkipTest. As a workaround
807             # mark the test as passed by exiting.
808             if getattr(unittest, 'SkipTest', None) is not None:
809                 raise unittest.SkipTest()
810             else:
811                 return
812
813         t = Task(self.tw, description="test task", due="eoy")
814         now = local_zone.localize(datetime.datetime.now())
815         eoy = local_zone.localize(datetime.datetime(
816             year=now.year,
817             month=12,
818             day=31,
819             hour=23,
820             minute=59,
821             second=59
822             ))
823         self.assertEqual(eoy, t['due'])
824
825     def test_complex_eoy_conversion(self):
826         if self.tw.version < six.text_type('2.4.0'):
827             # Python2.6 does not support SkipTest. As a workaround
828             # mark the test as passed by exiting.
829             if getattr(unittest, 'SkipTest', None) is not None:
830                 raise unittest.SkipTest()
831             else:
832                 return
833
834         t = Task(self.tw, description="test task", due="eoy - 4 months")
835         now = local_zone.localize(datetime.datetime.now())
836         due_date = local_zone.localize(datetime.datetime(
837             year=now.year,
838             month=9,
839             day=3,
840             hour=0,
841             minute=59,
842             second=59
843             ))
844         self.assertEqual(due_date, t['due'])
845
846     def test_filtering_with_string_datetime(self):
847         t = Task(self.tw, description="test task",
848                  due=datetime.datetime.now() - datetime.timedelta(0,2))
849         t.save()
850         self.assertEqual(len(self.tw.tasks.filter(due__before="now")), 1)
851
852 class AnnotationTest(TasklibTest):
853
854     def setUp(self):
855         super(AnnotationTest, self).setUp()
856         Task(self.tw, description="test task").save()
857
858     def test_adding_annotation(self):
859         task = self.tw.tasks.get()
860         task.add_annotation('test annotation')
861         self.assertEqual(len(task['annotations']), 1)
862         ann = task['annotations'][0]
863         self.assertEqual(ann['description'], 'test annotation')
864
865     def test_removing_annotation(self):
866         task = self.tw.tasks.get()
867         task.add_annotation('test annotation')
868         ann = task['annotations'][0]
869         ann.remove()
870         self.assertEqual(len(task['annotations']), 0)
871
872     def test_removing_annotation_by_description(self):
873         task = self.tw.tasks.get()
874         task.add_annotation('test annotation')
875         task.remove_annotation('test annotation')
876         self.assertEqual(len(task['annotations']), 0)
877
878     def test_removing_annotation_by_obj(self):
879         task = self.tw.tasks.get()
880         task.add_annotation('test annotation')
881         ann = task['annotations'][0]
882         task.remove_annotation(ann)
883         self.assertEqual(len(task['annotations']), 0)
884
885     def test_annotation_after_modification(self):
886          task = self.tw.tasks.get()
887          task['project'] = 'test'
888          task.add_annotation('I should really do this task')
889          self.assertEqual(task['project'], 'test')
890          task.save()
891          self.assertEqual(task['project'], 'test')
892
893     def test_serialize_annotations(self):
894         # Test that serializing annotations is possible
895         t = Task(self.tw, description="test")
896         t.save()
897
898         t.add_annotation("annotation1")
899         t.add_annotation("annotation2")
900
901         data = t._serialize('annotations', t._data['annotations'])
902
903         self.assertEqual(len(data), 2)
904         self.assertEqual(type(data[0]), dict)
905         self.assertEqual(type(data[1]), dict)
906
907         self.assertEqual(data[0]['description'], "annotation1")
908         self.assertEqual(data[1]['description'], "annotation2")
909
910
911 class UnicodeTest(TasklibTest):
912
913     def test_unicode_task(self):
914         Task(self.tw, description="†åßk").save()
915         self.tw.tasks.get()
916
917     def test_non_unicode_task(self):
918         Task(self.tw, description="test task").save()
919         self.tw.tasks.get()
920
921 class ReadOnlyDictViewTest(unittest.TestCase):
922
923     def setUp(self):
924         self.sample = dict(l=[1,2,3], d={'k':'v'})
925         self.original_sample = copy.deepcopy(self.sample)
926         self.view = ReadOnlyDictView(self.sample)
927
928     def test_readonlydictview_getitem(self):
929         l = self.view['l']
930         self.assertEqual(l, self.sample['l'])
931
932         # Assert that modification changed only copied value
933         l.append(4)
934         self.assertNotEqual(l, self.sample['l'])
935
936         # Assert that viewed dict is not changed
937         self.assertEqual(self.sample, self.original_sample)
938
939     def test_readonlydictview_contains(self):
940         self.assertEqual('l' in self.view, 'l' in self.sample)
941         self.assertEqual('d' in self.view, 'd' in self.sample)
942         self.assertEqual('k' in self.view, 'k' in self.sample)
943
944         # Assert that viewed dict is not changed
945         self.assertEqual(self.sample, self.original_sample)
946
947     def test_readonlydictview_iter(self):
948         self.assertEqual(list(k for k in self.view),
949                          list(k for k in self.sample))
950
951         # Assert the view is correct after modification
952         self.sample['new'] = 'value'
953         self.assertEqual(list(k for k in self.view),
954                          list(k for k in self.sample))
955
956     def test_readonlydictview_len(self):
957         self.assertEqual(len(self.view), len(self.sample))
958
959         # Assert the view is correct after modification
960         self.sample['new'] = 'value'
961         self.assertEqual(len(self.view), len(self.sample))
962
963     def test_readonlydictview_get(self):
964         l = self.view.get('l')
965         self.assertEqual(l, self.sample.get('l'))
966
967         # Assert that modification changed only copied value
968         l.append(4)
969         self.assertNotEqual(l, self.sample.get('l'))
970
971         # Assert that viewed dict is not changed
972         self.assertEqual(self.sample, self.original_sample)
973
974     def test_readonlydict_items(self):
975         view_items = self.view.items()
976         sample_items = list(self.sample.items())
977         self.assertEqual(view_items, sample_items)
978
979         view_items.append('newkey')
980         self.assertNotEqual(view_items, sample_items)
981         self.assertEqual(self.sample, self.original_sample)
982
983     def test_readonlydict_values(self):
984         view_values = self.view.values()
985         sample_values = list(self.sample.values())
986         self.assertEqual(view_values, sample_values)
987
988         view_list_item = list(filter(lambda x: type(x) is list,
989                                      view_values))[0]
990         view_list_item.append(4)
991         self.assertNotEqual(view_values, sample_values)
992         self.assertEqual(self.sample, self.original_sample)