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

TaskWarrior: Use config propery instead of get_config call
[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 .backends import TaskWarrior
15 from .task import Task, ReadOnlyDictView
16 from .serializing import DATE_FORMAT, local_zone
17
18 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
19 TASK_STANDARD_ATTRS = (
20     'status',
21     'uuid',
22     'entry',
23     'description',
24     'start',
25     'end',
26     'due',
27     'until',
28     'wait',
29     'modified',
30     'scheduled',
31     'recur',
32     'mask',
33     'imask',
34     'parent',
35     'project',
36     'priority',
37     'depends',
38     'tags',
39     'annotations',
40 )
41
42 total_seconds_2_6 = lambda x: x.microseconds / 1e6 + x.seconds + x.days * 24 * 3600
43
44
45 class TasklibTest(unittest.TestCase):
46
47     def setUp(self):
48         self.tmp = tempfile.mkdtemp(dir='.')
49         self.tw = TaskWarrior(data_location=self.tmp, taskrc_location='/')
50
51     def tearDown(self):
52         shutil.rmtree(self.tmp)
53
54
55 class TaskFilterTest(TasklibTest):
56
57     def test_all_empty(self):
58         self.assertEqual(len(self.tw.tasks.all()), 0)
59
60     def test_all_non_empty(self):
61         Task(self.tw, description="test task").save()
62         self.assertEqual(len(self.tw.tasks.all()), 1)
63         self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
64         self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
65
66     def test_pending_non_empty(self):
67         Task(self.tw, description="test task").save()
68         self.assertEqual(len(self.tw.tasks.pending()), 1)
69         self.assertEqual(self.tw.tasks.pending()[0]['description'],
70                          'test task')
71         self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
72
73     def test_completed_empty(self):
74         Task(self.tw, description="test task").save()
75         self.assertEqual(len(self.tw.tasks.completed()), 0)
76
77     def test_completed_non_empty(self):
78         Task(self.tw, description="test task").save()
79         self.assertEqual(len(self.tw.tasks.completed()), 0)
80         self.tw.tasks.all()[0].done()
81         self.assertEqual(len(self.tw.tasks.completed()), 1)
82
83     def test_filtering_by_attribute(self):
84         Task(self.tw, description="no priority task").save()
85         Task(self.tw, priority="H", description="high priority task").save()
86         self.assertEqual(len(self.tw.tasks.all()), 2)
87
88         # Assert that the correct number of tasks is returned
89         self.assertEqual(len(self.tw.tasks.filter(priority="H")), 1)
90
91         # Assert that the correct tasks are returned
92         high_priority_task = self.tw.tasks.get(priority="H")
93         self.assertEqual(high_priority_task['description'], "high priority task")
94
95     def test_filtering_by_empty_attribute(self):
96         Task(self.tw, description="no priority task").save()
97         Task(self.tw, priority="H", description="high priority task").save()
98         self.assertEqual(len(self.tw.tasks.all()), 2)
99
100         # Assert that the correct number of tasks is returned
101         self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
102
103         # Assert that the correct tasks are returned
104         no_priority_task = self.tw.tasks.get(priority=None)
105         self.assertEqual(no_priority_task['description'], "no priority task")
106
107     def test_filter_for_task_with_space_in_descripition(self):
108         task = Task(self.tw, description="test task")
109         task.save()
110
111         filtered_task = self.tw.tasks.get(description="test task")
112         self.assertEqual(filtered_task['description'], "test task")
113
114     def test_filter_for_task_without_space_in_descripition(self):
115         task = Task(self.tw, description="test")
116         task.save()
117
118         filtered_task = self.tw.tasks.get(description="test")
119         self.assertEqual(filtered_task['description'], "test")
120
121     def test_filter_for_task_with_space_in_project(self):
122         task = Task(self.tw, description="test", project="random project")
123         task.save()
124
125         filtered_task = self.tw.tasks.get(project="random project")
126         self.assertEqual(filtered_task['project'], "random project")
127
128     def test_filter_for_task_without_space_in_project(self):
129         task = Task(self.tw, description="test", project="random")
130         task.save()
131
132         filtered_task = self.tw.tasks.get(project="random")
133         self.assertEqual(filtered_task['project'], "random")
134
135     def test_filter_with_empty_uuid(self):
136         self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid=''))
137
138     def test_filter_dummy_by_status(self):
139         t = Task(self.tw, description="test")
140         t.save()
141
142         tasks = self.tw.tasks.filter(status=t['status'])
143         self.assertEqual(list(tasks), [t])
144
145     def test_filter_dummy_by_uuid(self):
146         t = Task(self.tw, description="test")
147         t.save()
148
149         tasks = self.tw.tasks.filter(uuid=t['uuid'])
150         self.assertEqual(list(tasks), [t])
151
152     def test_filter_dummy_by_entry(self):
153         t = Task(self.tw, description="test")
154         t.save()
155
156         tasks = self.tw.tasks.filter(entry=t['entry'])
157         self.assertEqual(list(tasks), [t])
158
159     def test_filter_dummy_by_description(self):
160         t = Task(self.tw, description="test")
161         t.save()
162
163         tasks = self.tw.tasks.filter(description=t['description'])
164         self.assertEqual(list(tasks), [t])
165
166     def test_filter_dummy_by_start(self):
167         t = Task(self.tw, description="test")
168         t.save()
169         t.start()
170
171         tasks = self.tw.tasks.filter(start=t['start'])
172         self.assertEqual(list(tasks), [t])
173
174     def test_filter_dummy_by_end(self):
175         t = Task(self.tw, description="test")
176         t.save()
177         t.done()
178
179         tasks = self.tw.tasks.filter(end=t['end'])
180         self.assertEqual(list(tasks), [t])
181
182     def test_filter_dummy_by_due(self):
183         t = Task(self.tw, description="test", due=datetime.datetime.now())
184         t.save()
185
186         tasks = self.tw.tasks.filter(due=t['due'])
187         self.assertEqual(list(tasks), [t])
188
189     def test_filter_dummy_by_until(self):
190         t = Task(self.tw, description="test")
191         t.save()
192
193         tasks = self.tw.tasks.filter(until=t['until'])
194         self.assertEqual(list(tasks), [t])
195
196     def test_filter_dummy_by_modified(self):
197         # Older TW version does not support bumping modified
198         # on save
199         if self.tw.version < six.text_type('2.2.0'):
200             # Python2.6 does not support SkipTest. As a workaround
201             # mark the test as passed by exiting.
202             if getattr(unittest, 'SkipTest', None) is not None:
203                 raise unittest.SkipTest()
204             else:
205                 return
206
207         t = Task(self.tw, description="test")
208         t.save()
209
210         tasks = self.tw.tasks.filter(modified=t['modified'])
211         self.assertEqual(list(tasks), [t])
212
213     def test_filter_dummy_by_scheduled(self):
214         t = Task(self.tw, description="test")
215         t.save()
216
217         tasks = self.tw.tasks.filter(scheduled=t['scheduled'])
218         self.assertEqual(list(tasks), [t])
219
220     def test_filter_dummy_by_tags(self):
221         t = Task(self.tw, description="test", tags=["home"])
222         t.save()
223
224         tasks = self.tw.tasks.filter(tags=t['tags'])
225         self.assertEqual(list(tasks), [t])
226
227     def test_filter_dummy_by_projects(self):
228         t = Task(self.tw, description="test", project="random")
229         t.save()
230
231         tasks = self.tw.tasks.filter(project=t['project'])
232         self.assertEqual(list(tasks), [t])
233
234     def test_filter_by_priority(self):
235         t = Task(self.tw, description="test", priority="H")
236         t.save()
237
238         tasks = self.tw.tasks.filter(priority=t['priority'])
239         self.assertEqual(list(tasks), [t])
240
241
242 class TaskTest(TasklibTest):
243
244     def test_create_unsaved_task(self):
245         # Make sure a new task is not saved unless explicitly called for
246         t = Task(self.tw, description="test task")
247         self.assertEqual(len(self.tw.tasks.all()), 0)
248
249     # TODO: once python 2.6 compatiblity is over, use context managers here
250     #       and in all subsequent tests for assertRaises
251
252     def test_delete_unsaved_task(self):
253         t = Task(self.tw, description="test task")
254         self.assertRaises(Task.NotSaved, t.delete)
255
256     def test_complete_unsaved_task(self):
257         t = Task(self.tw, description="test task")
258         self.assertRaises(Task.NotSaved, t.done)
259
260     def test_refresh_unsaved_task(self):
261         t = Task(self.tw, description="test task")
262         self.assertRaises(Task.NotSaved, t.refresh)
263
264     def test_start_unsaved_task(self):
265         t = Task(self.tw, description="test task")
266         self.assertRaises(Task.NotSaved, t.start)
267
268     def test_delete_deleted_task(self):
269         t = Task(self.tw, description="test task")
270         t.save()
271         t.delete()
272
273         self.assertRaises(Task.DeletedTask, t.delete)
274
275     def test_complete_completed_task(self):
276         t = Task(self.tw, description="test task")
277         t.save()
278         t.done()
279
280         self.assertRaises(Task.CompletedTask, t.done)
281
282     def test_start_completed_task(self):
283         t = Task(self.tw, description="test task")
284         t.save()
285         t.done()
286
287         self.assertRaises(Task.CompletedTask, t.start)
288
289     def test_add_completed_task(self):
290         t = Task(self.tw, description="test", status="completed",
291                  end=datetime.datetime.now())
292         t.save()
293
294     def test_add_multiple_completed_tasks(self):
295         t1 = Task(self.tw, description="test1", status="completed",
296                  end=datetime.datetime.now())
297         t2 = Task(self.tw, description="test2", status="completed",
298                  end=datetime.datetime.now())
299         t1.save()
300         t2.save()
301
302     def test_complete_deleted_task(self):
303         t = Task(self.tw, description="test task")
304         t.save()
305         t.delete()
306
307         self.assertRaises(Task.DeletedTask, t.done)
308
309     def test_starting_task(self):
310         t = Task(self.tw, description="test task")
311         now = t.datetime_normalizer(datetime.datetime.now())
312         t.save()
313         t.start()
314
315         self.assertTrue(now.replace(microsecond=0) <= t['start'])
316         self.assertEqual(t['status'], 'pending')
317
318     def test_completing_task(self):
319         t = Task(self.tw, description="test task")
320         now = t.datetime_normalizer(datetime.datetime.now())
321         t.save()
322         t.done()
323
324         self.assertTrue(now.replace(microsecond=0) <= t['end'])
325         self.assertEqual(t['status'], 'completed')
326
327     def test_deleting_task(self):
328         t = Task(self.tw, description="test task")
329         now = t.datetime_normalizer(datetime.datetime.now())
330         t.save()
331         t.delete()
332
333         self.assertTrue(now.replace(microsecond=0) <= t['end'])
334         self.assertEqual(t['status'], 'deleted')
335
336     def test_started_task_active(self):
337         t = Task(self.tw, description="test task")
338         t.save()
339         t.start()
340         self.assertTrue(t.active)
341
342     def test_unstarted_task_inactive(self):
343         t = Task(self.tw, description="test task")
344         self.assertFalse(t.active)
345         t.save()
346         self.assertFalse(t.active)
347
348     def test_start_active_task(self):
349         t = Task(self.tw, description="test task")
350         t.save()
351         t.start()
352         self.assertRaises(Task.ActiveTask, t.start)
353
354     def test_stop_completed_task(self):
355         t = Task(self.tw, description="test task")
356         t.save()
357         t.start()
358         t.done()
359
360         self.assertRaises(Task.InactiveTask, t.stop)
361
362         t = Task(self.tw, description="test task")
363         t.save()
364         t.done()
365
366         self.assertRaises(Task.InactiveTask, t.stop)
367
368     def test_stop_deleted_task(self):
369         t = Task(self.tw, description="test task")
370         t.save()
371         t.start()
372         t.delete()
373         t.stop()
374
375     def test_stop_inactive_task(self):
376         t = Task(self.tw, description="test task")
377         t.save()
378
379         self.assertRaises(Task.InactiveTask, t.stop)
380
381         t = Task(self.tw, description="test task")
382         t.save()
383         t.start()
384         t.stop()
385
386         self.assertRaises(Task.InactiveTask, t.stop)
387
388     def test_stopping_task(self):
389         t = Task(self.tw, description="test task")
390         now = t.datetime_normalizer(datetime.datetime.now())
391         t.save()
392         t.start()
393         t.stop()
394
395         self.assertEqual(t['end'], None)
396         self.assertEqual(t['status'], 'pending')
397         self.assertFalse(t.active)
398
399     def test_modify_simple_attribute_without_space(self):
400         t = Task(self.tw, description="test")
401         t.save()
402
403         self.assertEquals(t['description'], "test")
404
405         t['description'] = "test-modified"
406         t.save()
407
408         self.assertEquals(t['description'], "test-modified")
409
410     def test_modify_simple_attribute_with_space(self):
411         # Space can pose problems with parsing
412         t = Task(self.tw, description="test task")
413         t.save()
414
415         self.assertEquals(t['description'], "test task")
416
417         t['description'] = "test task modified"
418         t.save()
419
420         self.assertEquals(t['description'], "test task modified")
421
422     def test_empty_dependency_set_of_unsaved_task(self):
423         t = Task(self.tw, description="test task")
424         self.assertEqual(t['depends'], set())
425
426     def test_empty_dependency_set_of_saved_task(self):
427         t = Task(self.tw, description="test task")
428         t.save()
429         self.assertEqual(t['depends'], set())
430
431     def test_set_unsaved_task_as_dependency(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         # We only save the parent task, dependency task is unsaved
437         t.save()
438         t['depends'] = set([dependency])
439
440         self.assertRaises(Task.NotSaved, t.save)
441
442     def test_set_simple_dependency_set(self):
443         # Adds only one dependency to task with no dependencies
444         t = Task(self.tw, description="test task")
445         dependency = Task(self.tw, description="needs to be done first")
446
447         t.save()
448         dependency.save()
449
450         t['depends'] = set([dependency])
451
452         self.assertEqual(t['depends'], set([dependency]))
453
454     def test_set_complex_dependency_set(self):
455         # Adds two dependencies to task with no dependencies
456         t = Task(self.tw, description="test task")
457         dependency1 = Task(self.tw, description="needs to be done first")
458         dependency2 = Task(self.tw, description="needs to be done second")
459
460         t.save()
461         dependency1.save()
462         dependency2.save()
463
464         t['depends'] = set([dependency1, dependency2])
465
466         self.assertEqual(t['depends'], set([dependency1, dependency2]))
467
468     def test_remove_from_dependency_set(self):
469         # Removes dependency from task with two dependencies
470         t = Task(self.tw, description="test task")
471         dependency1 = Task(self.tw, description="needs to be done first")
472         dependency2 = Task(self.tw, description="needs to be done second")
473
474         dependency1.save()
475         dependency2.save()
476
477         t['depends'] = set([dependency1, dependency2])
478         t.save()
479
480         t['depends'].remove(dependency2)
481         t.save()
482
483         self.assertEqual(t['depends'], set([dependency1]))
484
485     def test_add_to_dependency_set(self):
486         # Adds dependency to task with one dependencies
487         t = Task(self.tw, description="test task")
488         dependency1 = Task(self.tw, description="needs to be done first")
489         dependency2 = Task(self.tw, description="needs to be done second")
490
491         dependency1.save()
492         dependency2.save()
493
494         t['depends'] = set([dependency1])
495         t.save()
496
497         t['depends'].add(dependency2)
498         t.save()
499
500         self.assertEqual(t['depends'], set([dependency1, dependency2]))
501
502     def test_add_to_empty_dependency_set(self):
503         # Adds dependency to task with one dependencies
504         t = Task(self.tw, description="test task")
505         dependency = Task(self.tw, description="needs to be done first")
506
507         dependency.save()
508
509         t['depends'].add(dependency)
510         t.save()
511
512         self.assertEqual(t['depends'], set([dependency]))
513
514     def test_simple_dependency_set_save_repeatedly(self):
515         # Adds only one dependency to task with no dependencies
516         t = Task(self.tw, description="test task")
517         dependency = Task(self.tw, description="needs to be done first")
518         dependency.save()
519
520         t['depends'] = set([dependency])
521         t.save()
522
523         # We taint the task, but keep depends intact
524         t['description'] = "test task modified"
525         t.save()
526
527         self.assertEqual(t['depends'], set([dependency]))
528
529         # We taint the task, but assign the same set to the depends
530         t['depends'] = set([dependency])
531         t['description'] = "test task modified again"
532         t.save()
533
534         self.assertEqual(t['depends'], set([dependency]))
535
536     def test_compare_different_tasks(self):
537         # Negative: compare two different tasks
538         t1 = Task(self.tw, description="test task")
539         t2 = Task(self.tw, description="test task")
540
541         t1.save()
542         t2.save()
543
544         self.assertEqual(t1 == t2, False)
545
546     def test_compare_same_task_object(self):
547         # Compare Task object wit itself
548         t = Task(self.tw, description="test task")
549         t.save()
550
551         self.assertEqual(t == t, True)
552
553     def test_compare_same_task(self):
554         # Compare the same task using two different objects
555         t1 = Task(self.tw, description="test task")
556         t1.save()
557
558         t2 = self.tw.tasks.get(uuid=t1['uuid'])
559         self.assertEqual(t1 == t2, True)
560
561     def test_compare_unsaved_tasks(self):
562         # t1 and t2 are unsaved tasks, considered to be unequal
563         # despite the content of data
564         t1 = Task(self.tw, description="test task")
565         t2 = Task(self.tw, description="test task")
566
567         self.assertEqual(t1 == t2, False)
568
569     def test_hash_unsaved_tasks(self):
570         # Considered equal, it's the same object
571         t1 = Task(self.tw, description="test task")
572         t2 = t1
573         self.assertEqual(hash(t1) == hash(t2), True)
574
575     def test_hash_same_task(self):
576         # Compare the hash of the task using two different objects
577         t1 = Task(self.tw, description="test task")
578         t1.save()
579
580         t2 = self.tw.tasks.get(uuid=t1['uuid'])
581         self.assertEqual(t1.__hash__(), t2.__hash__())
582
583     def test_adding_task_with_priority(self):
584         t = Task(self.tw, description="test task", priority="M")
585         t.save()
586
587     def test_removing_priority_with_none(self):
588         t = Task(self.tw, description="test task", priority="L")
589         t.save()
590
591         # Remove the priority mark
592         t['priority'] = None
593         t.save()
594
595         # Assert that priority is not there after saving
596         self.assertEqual(t['priority'], None)
597
598     def test_adding_task_with_due_time(self):
599         t = Task(self.tw, description="test task", due=datetime.datetime.now())
600         t.save()
601
602     def test_removing_due_time_with_none(self):
603         t = Task(self.tw, description="test task", due=datetime.datetime.now())
604         t.save()
605
606         # Remove the due timestamp
607         t['due'] = None
608         t.save()
609
610         # Assert that due timestamp is no longer there
611         self.assertEqual(t['due'], None)
612
613     def test_modified_fields_new_task(self):
614         t = Task(self.tw)
615
616         # This should be empty with new task
617         self.assertEqual(set(t._modified_fields), set())
618
619         # Modify the task
620         t['description'] = "test task"
621         self.assertEqual(set(t._modified_fields), set(['description']))
622
623         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
624         self.assertEqual(set(t._modified_fields), set(['description', 'due']))
625
626         t['project'] = "test project"
627         self.assertEqual(set(t._modified_fields),
628                          set(['description', 'due', 'project']))
629
630         # List of modified fields should clear out when saved
631         t.save()
632         self.assertEqual(set(t._modified_fields), set())
633
634         # Reassigning the fields with the same values now should not produce
635         # modified fields
636         t['description'] = "test task"
637         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
638         t['project'] = "test project"
639         self.assertEqual(set(t._modified_fields), set())
640
641     def test_modified_fields_loaded_task(self):
642         t = Task(self.tw)
643
644         # Modify the task
645         t['description'] = "test task"
646         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
647         t['project'] = "test project"
648
649         dependency = Task(self.tw, description="dependency")
650         dependency.save()
651         t['depends'] = set([dependency])
652
653         # List of modified fields should clear out when saved
654         t.save()
655         self.assertEqual(set(t._modified_fields), set())
656
657         # Get the task by using a filter by UUID
658         t2 = self.tw.tasks.get(uuid=t['uuid'])
659
660         # Reassigning the fields with the same values now should not produce
661         # modified fields
662         t['description'] = "test task"
663         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
664         t['project'] = "test project"
665         t['depends'] = set([dependency])
666         self.assertEqual(set(t._modified_fields), set())
667
668     def test_modified_fields_not_affected_by_reading(self):
669         t = Task(self.tw)
670
671         for field in TASK_STANDARD_ATTRS:
672             value = t[field]
673
674         self.assertEqual(set(t._modified_fields), set())
675
676     def test_setting_read_only_attrs_through_init(self):
677         # Test that we are unable to set readonly attrs through __init__
678         for readonly_key in Task.read_only_fields:
679             kwargs = {'description': 'test task', readonly_key: 'value'}
680             self.assertRaises(RuntimeError,
681                               lambda: Task(self.tw, **kwargs))
682
683     def test_setting_read_only_attrs_through_setitem(self):
684         # Test that we are unable to set readonly attrs through __init__
685         for readonly_key in Task.read_only_fields:
686             t = Task(self.tw, description='test task')
687             self.assertRaises(RuntimeError,
688                               lambda: t.__setitem__(readonly_key, 'value'))
689
690     def test_saving_unmodified_task(self):
691         t = Task(self.tw, description="test task")
692         t.save()
693         t.save()
694
695     def test_adding_tag_by_appending(self):
696         t = Task(self.tw, description="test task", tags=['test1'])
697         t.save()
698         t['tags'].append('test2')
699         t.save()
700         self.assertEqual(t['tags'], ['test1', 'test2'])
701
702     def test_adding_tag_by_appending_empty(self):
703         t = Task(self.tw, description="test task")
704         t.save()
705         t['tags'].append('test')
706         t.save()
707         self.assertEqual(t['tags'], ['test'])
708
709     def test_serializers_returning_empty_string_for_none(self):
710         # Test that any serializer returns '' when passed None
711         t = Task(self.tw)
712         serializers = [getattr(t, serializer_name) for serializer_name in
713                        filter(lambda x: x.startswith('serialize_'), dir(t))]
714         for serializer in serializers:
715             self.assertEqual(serializer(None), '')
716
717     def test_deserializer_returning_empty_value_for_empty_string(self):
718         # Test that any deserializer returns empty value when passed ''
719         t = Task(self.tw)
720         deserializers = [getattr(t, deserializer_name) for deserializer_name in
721                         filter(lambda x: x.startswith('deserialize_'), dir(t))]
722         for deserializer in deserializers:
723             self.assertTrue(deserializer('') in (None, [], set()))
724
725     def test_normalizers_handling_none(self):
726         # Test that any normalizer can handle None as a valid value
727         t = Task(self.tw)
728
729         for key in TASK_STANDARD_ATTRS:
730             t._normalize(key, None)
731
732     def test_recurrent_task_generation(self):
733         today = datetime.date.today()
734         t = Task(self.tw, description="brush teeth",
735                  due=today, recur="daily")
736         t.save()
737         self.assertEqual(len(self.tw.tasks.pending()), 2)
738
739     def test_modify_number_of_tasks_at_once(self):
740         for i in range(1, 100):
741             Task(self.tw, description="test task %d" % i, tags=['test']).save()
742
743         self.tw.execute_command(['+test', 'mod', 'unified', 'description'])
744
745     def test_return_all_from_executed_command(self):
746         Task(self.tw, description="test task", tags=['test']).save()
747         out, err, rc = self.tw.execute_command(['count'], return_all=True)
748         self.assertEqual(rc, 0)
749
750     def test_return_all_from_failed_executed_command(self):
751         Task(self.tw, description="test task", tags=['test']).save()
752         out, err, rc = self.tw.execute_command(['countinvalid'],
753             return_all=True, allow_failure=False)
754         self.assertNotEqual(rc, 0)
755
756
757 class TaskFromHookTest(TasklibTest):
758
759     input_add_data = six.StringIO(
760         '{"description":"Buy some milk",'
761         '"entry":"20141118T050231Z",'
762         '"status":"pending",'
763         '"start":"20141119T152233Z",'
764         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
765
766     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
767         '{"description":"Buy some milk finally",'
768         '"entry":"20141118T050231Z",'
769         '"status":"completed",'
770         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
771
772     exported_raw_data = (
773         '{"project":"Home",'
774          '"due":"20150101T232323Z",'
775          '"description":"test task"}')
776
777     def test_setting_up_from_add_hook_input(self):
778         t = Task.from_input(input_file=self.input_add_data, backend=self.tw)
779         self.assertEqual(t['description'], "Buy some milk")
780         self.assertEqual(t.pending, True)
781
782     def test_setting_up_from_modified_hook_input(self):
783         t = Task.from_input(input_file=self.input_modify_data, modify=True,
784                             backend=self.tw)
785         self.assertEqual(t['description'], "Buy some milk finally")
786         self.assertEqual(t.pending, False)
787         self.assertEqual(t.completed, True)
788
789         self.assertEqual(t._original_data['status'], "pending")
790         self.assertEqual(t._original_data['description'], "Buy some milk")
791         self.assertEqual(set(t._modified_fields),
792                          set(['status', 'description', 'start']))
793
794     def test_export_data(self):
795         t = Task(self.tw, description="test task",
796             project="Home",
797             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
798
799         # Check that the output is a permutation of:
800         # {"project":"Home","description":"test task","due":"20150101232323Z"}
801         allowed_segments = self.exported_raw_data[1:-1].split(',')
802         allowed_output = [
803             '{' + ','.join(segments) + '}'
804             for segments in itertools.permutations(allowed_segments)
805         ]
806
807         self.assertTrue(any(t.export_data() == expected
808                             for expected in allowed_output))
809
810 class TimezoneAwareDatetimeTest(TasklibTest):
811
812     def setUp(self):
813         super(TimezoneAwareDatetimeTest, self).setUp()
814         self.zone = local_zone
815         self.localdate_naive = datetime.datetime(2015,2,2)
816         self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
817         self.localtime_aware = self.zone.localize(self.localtime_naive)
818         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
819
820     def test_timezone_naive_datetime_setitem(self):
821         t = Task(self.tw, description="test task")
822         t['due'] = self.localtime_naive
823         self.assertEqual(t['due'], self.localtime_aware)
824
825     def test_timezone_naive_datetime_using_init(self):
826         t = Task(self.tw, description="test task", due=self.localtime_naive)
827         self.assertEqual(t['due'], self.localtime_aware)
828
829     def test_filter_by_naive_datetime(self):
830         t = Task(self.tw, description="task1", due=self.localtime_naive)
831         t.save()
832         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
833         self.assertEqual(len(matching_tasks), 1)
834
835     def test_serialize_naive_datetime(self):
836         t = Task(self.tw, description="task1", due=self.localtime_naive)
837         self.assertEqual(json.loads(t.export_data())['due'],
838                          self.utctime_aware.strftime(DATE_FORMAT))
839
840     def test_timezone_naive_date_setitem(self):
841         t = Task(self.tw, description="test task")
842         t['due'] = self.localdate_naive
843         self.assertEqual(t['due'], self.localtime_aware)
844
845     def test_timezone_naive_date_using_init(self):
846         t = Task(self.tw, description="test task", due=self.localdate_naive)
847         self.assertEqual(t['due'], self.localtime_aware)
848
849     def test_filter_by_naive_date(self):
850         t = Task(self.tw, description="task1", due=self.localdate_naive)
851         t.save()
852         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
853         self.assertEqual(len(matching_tasks), 1)
854
855     def test_serialize_naive_date(self):
856         t = Task(self.tw, description="task1", due=self.localdate_naive)
857         self.assertEqual(json.loads(t.export_data())['due'],
858                          self.utctime_aware.strftime(DATE_FORMAT))
859
860     def test_timezone_aware_datetime_setitem(self):
861         t = Task(self.tw, description="test task")
862         t['due'] = self.localtime_aware
863         self.assertEqual(t['due'], self.localtime_aware)
864
865     def test_timezone_aware_datetime_using_init(self):
866         t = Task(self.tw, description="test task", due=self.localtime_aware)
867         self.assertEqual(t['due'], self.localtime_aware)
868
869     def test_filter_by_aware_datetime(self):
870         t = Task(self.tw, description="task1", due=self.localtime_aware)
871         t.save()
872         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
873         self.assertEqual(len(matching_tasks), 1)
874
875     def test_serialize_aware_datetime(self):
876         t = Task(self.tw, description="task1", due=self.localtime_aware)
877         self.assertEqual(json.loads(t.export_data())['due'],
878                          self.utctime_aware.strftime(DATE_FORMAT))
879
880 class DatetimeStringTest(TasklibTest):
881
882     def test_simple_now_conversion(self):
883         if self.tw.version < six.text_type('2.4.0'):
884             # Python2.6 does not support SkipTest. As a workaround
885             # mark the test as passed by exiting.
886             if getattr(unittest, 'SkipTest', None) is not None:
887                 raise unittest.SkipTest()
888             else:
889                 return
890
891         t = Task(self.tw, description="test task", due="now")
892         now = local_zone.localize(datetime.datetime.now())
893
894         # Assert that both times are not more than 5 seconds apart
895         if sys.version_info < (2,7):
896             self.assertTrue(total_seconds_2_6(now - t['due']) < 5)
897             self.assertTrue(total_seconds_2_6(t['due'] - now) < 5)
898         else:
899             self.assertTrue((now - t['due']).total_seconds() < 5)
900             self.assertTrue((t['due'] - now).total_seconds() < 5)
901
902     def test_simple_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")
912         now = local_zone.localize(datetime.datetime.now())
913         eoy = 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             ))
921         self.assertEqual(eoy, t['due'])
922
923     def test_complex_eoy_conversion(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", due="eoy - 4 months")
933         now = local_zone.localize(datetime.datetime.now())
934         due_date = local_zone.localize(datetime.datetime(
935             year=now.year,
936             month=12,
937             day=31,
938             hour=23,
939             minute=59,
940             second=59
941             )) - datetime.timedelta(0,4 * 30 * 86400)
942         self.assertEqual(due_date, t['due'])
943
944     def test_filtering_with_string_datetime(self):
945         if self.tw.version < six.text_type('2.4.0'):
946             # Python2.6 does not support SkipTest. As a workaround
947             # mark the test as passed by exiting.
948             if getattr(unittest, 'SkipTest', None) is not None:
949                 raise unittest.SkipTest()
950             else:
951                 return
952
953         t = Task(self.tw, description="test task",
954                  due=datetime.datetime.now() - datetime.timedelta(0,2))
955         t.save()
956         self.assertEqual(len(self.tw.tasks.filter(due__before="now")), 1)
957
958 class AnnotationTest(TasklibTest):
959
960     def setUp(self):
961         super(AnnotationTest, self).setUp()
962         Task(self.tw, description="test task").save()
963
964     def test_adding_annotation(self):
965         task = self.tw.tasks.get()
966         task.add_annotation('test annotation')
967         self.assertEqual(len(task['annotations']), 1)
968         ann = task['annotations'][0]
969         self.assertEqual(ann['description'], 'test annotation')
970
971     def test_removing_annotation(self):
972         task = self.tw.tasks.get()
973         task.add_annotation('test annotation')
974         ann = task['annotations'][0]
975         ann.remove()
976         self.assertEqual(len(task['annotations']), 0)
977
978     def test_removing_annotation_by_description(self):
979         task = self.tw.tasks.get()
980         task.add_annotation('test annotation')
981         task.remove_annotation('test annotation')
982         self.assertEqual(len(task['annotations']), 0)
983
984     def test_removing_annotation_by_obj(self):
985         task = self.tw.tasks.get()
986         task.add_annotation('test annotation')
987         ann = task['annotations'][0]
988         task.remove_annotation(ann)
989         self.assertEqual(len(task['annotations']), 0)
990
991     def test_annotation_after_modification(self):
992          task = self.tw.tasks.get()
993          task['project'] = 'test'
994          task.add_annotation('I should really do this task')
995          self.assertEqual(task['project'], 'test')
996          task.save()
997          self.assertEqual(task['project'], 'test')
998
999     def test_serialize_annotations(self):
1000         # Test that serializing annotations is possible
1001         t = Task(self.tw, description="test")
1002         t.save()
1003
1004         t.add_annotation("annotation1")
1005         t.add_annotation("annotation2")
1006
1007         data = t._serialize('annotations', t._data['annotations'])
1008
1009         self.assertEqual(len(data), 2)
1010         self.assertEqual(type(data[0]), dict)
1011         self.assertEqual(type(data[1]), dict)
1012
1013         self.assertEqual(data[0]['description'], "annotation1")
1014         self.assertEqual(data[1]['description'], "annotation2")
1015
1016
1017 class UnicodeTest(TasklibTest):
1018
1019     def test_unicode_task(self):
1020         Task(self.tw, description=six.u("†åßk")).save()
1021         self.tw.tasks.get()
1022
1023     def test_filter_by_unicode_task(self):
1024         Task(self.tw, description=six.u("†åßk")).save()
1025         tasks = self.tw.tasks.filter(description=six.u("†åßk"))
1026         self.assertEqual(len(tasks), 1)
1027
1028     def test_non_unicode_task(self):
1029         Task(self.tw, description="test task").save()
1030         self.tw.tasks.get()
1031
1032 class ReadOnlyDictViewTest(unittest.TestCase):
1033
1034     def setUp(self):
1035         self.sample = dict(l=[1,2,3], d={'k':'v'})
1036         self.original_sample = copy.deepcopy(self.sample)
1037         self.view = ReadOnlyDictView(self.sample)
1038
1039     def test_readonlydictview_getitem(self):
1040         l = self.view['l']
1041         self.assertEqual(l, self.sample['l'])
1042
1043         # Assert that modification changed only copied value
1044         l.append(4)
1045         self.assertNotEqual(l, self.sample['l'])
1046
1047         # Assert that viewed dict is not changed
1048         self.assertEqual(self.sample, self.original_sample)
1049
1050     def test_readonlydictview_contains(self):
1051         self.assertEqual('l' in self.view, 'l' in self.sample)
1052         self.assertEqual('d' in self.view, 'd' in self.sample)
1053         self.assertEqual('k' in self.view, 'k' in self.sample)
1054
1055         # Assert that viewed dict is not changed
1056         self.assertEqual(self.sample, self.original_sample)
1057
1058     def test_readonlydictview_iter(self):
1059         self.assertEqual(list(k for k in self.view),
1060                          list(k for k in self.sample))
1061
1062         # Assert the view is correct after modification
1063         self.sample['new'] = 'value'
1064         self.assertEqual(list(k for k in self.view),
1065                          list(k for k in self.sample))
1066
1067     def test_readonlydictview_len(self):
1068         self.assertEqual(len(self.view), len(self.sample))
1069
1070         # Assert the view is correct after modification
1071         self.sample['new'] = 'value'
1072         self.assertEqual(len(self.view), len(self.sample))
1073
1074     def test_readonlydictview_get(self):
1075         l = self.view.get('l')
1076         self.assertEqual(l, self.sample.get('l'))
1077
1078         # Assert that modification changed only copied value
1079         l.append(4)
1080         self.assertNotEqual(l, self.sample.get('l'))
1081
1082         # Assert that viewed dict is not changed
1083         self.assertEqual(self.sample, self.original_sample)
1084
1085     def test_readonlydict_items(self):
1086         view_items = self.view.items()
1087         sample_items = list(self.sample.items())
1088         self.assertEqual(view_items, sample_items)
1089
1090         view_items.append('newkey')
1091         self.assertNotEqual(view_items, sample_items)
1092         self.assertEqual(self.sample, self.original_sample)
1093
1094     def test_readonlydict_values(self):
1095         view_values = self.view.values()
1096         sample_values = list(self.sample.values())
1097         self.assertEqual(view_values, sample_values)
1098
1099         view_list_item = list(filter(lambda x: type(x) is list,
1100                                      view_values))[0]
1101         view_list_item.append(4)
1102         self.assertNotEqual(view_values, sample_values)
1103         self.assertEqual(self.sample, self.original_sample)