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

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