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

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