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

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