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

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