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