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

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