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

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