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

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