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

eb199487c0d811f019d7f91ea4a455ea7561923a
[etc/taskwarrior.git] / tasklib / tests.py
1 # coding=utf-8
2
3 import datetime
4 import itertools
5 import json
6 import pytz
7 import six
8 import shutil
9 import tempfile
10 import unittest
11
12 from .task import TaskWarrior, Task
13
14 # http://taskwarrior.org/docs/design/task.html , Section: The Attributes
15 TASK_STANDARD_ATTRS = (
16     'status',
17     'uuid',
18     'entry',
19     'description',
20     'start',
21     'end',
22     'due',
23     'until',
24     'wait',
25     'modified',
26     'scheduled',
27     'recur',
28     'mask',
29     'imask',
30     'parent',
31     'project',
32     'priority',
33     'depends',
34     'tags',
35     'annotations',
36 )
37
38 class TasklibTest(unittest.TestCase):
39
40     def setUp(self):
41         self.tmp = tempfile.mkdtemp(dir='.')
42         self.tw = TaskWarrior(data_location=self.tmp)
43
44     def tearDown(self):
45         shutil.rmtree(self.tmp)
46
47
48 class TaskFilterTest(TasklibTest):
49
50     def test_all_empty(self):
51         self.assertEqual(len(self.tw.tasks.all()), 0)
52
53     def test_all_non_empty(self):
54         Task(self.tw, description="test task").save()
55         self.assertEqual(len(self.tw.tasks.all()), 1)
56         self.assertEqual(self.tw.tasks.all()[0]['description'], 'test task')
57         self.assertEqual(self.tw.tasks.all()[0]['status'], 'pending')
58
59     def test_pending_non_empty(self):
60         Task(self.tw, description="test task").save()
61         self.assertEqual(len(self.tw.tasks.pending()), 1)
62         self.assertEqual(self.tw.tasks.pending()[0]['description'],
63                          'test task')
64         self.assertEqual(self.tw.tasks.pending()[0]['status'], 'pending')
65
66     def test_completed_empty(self):
67         Task(self.tw, description="test task").save()
68         self.assertEqual(len(self.tw.tasks.completed()), 0)
69
70     def test_completed_non_empty(self):
71         Task(self.tw, description="test task").save()
72         self.assertEqual(len(self.tw.tasks.completed()), 0)
73         self.tw.tasks.all()[0].done()
74         self.assertEqual(len(self.tw.tasks.completed()), 1)
75
76     def test_filtering_by_attribute(self):
77         Task(self.tw, description="no priority task").save()
78         Task(self.tw, priority="H", description="high priority task").save()
79         self.assertEqual(len(self.tw.tasks.all()), 2)
80
81         # Assert that the correct number of tasks is returned
82         self.assertEqual(len(self.tw.tasks.filter(priority="H")), 1)
83
84         # Assert that the correct tasks are returned
85         high_priority_task = self.tw.tasks.get(priority="H")
86         self.assertEqual(high_priority_task['description'], "high priority task")
87
88     def test_filtering_by_empty_attribute(self):
89         Task(self.tw, description="no priority task").save()
90         Task(self.tw, priority="H", description="high priority task").save()
91         self.assertEqual(len(self.tw.tasks.all()), 2)
92
93         # Assert that the correct number of tasks is returned
94         self.assertEqual(len(self.tw.tasks.filter(priority=None)), 1)
95
96         # Assert that the correct tasks are returned
97         no_priority_task = self.tw.tasks.get(priority=None)
98         self.assertEqual(no_priority_task['description'], "no priority task")
99
100     def test_filter_for_task_with_space_in_descripition(self):
101         task = Task(self.tw, description="test task")
102         task.save()
103
104         filtered_task = self.tw.tasks.get(description="test task")
105         self.assertEqual(filtered_task['description'], "test task")
106
107     def test_filter_for_task_without_space_in_descripition(self):
108         task = Task(self.tw, description="test")
109         task.save()
110
111         filtered_task = self.tw.tasks.get(description="test")
112         self.assertEqual(filtered_task['description'], "test")
113
114     def test_filter_for_task_with_space_in_project(self):
115         task = Task(self.tw, description="test", project="random project")
116         task.save()
117
118         filtered_task = self.tw.tasks.get(project="random project")
119         self.assertEqual(filtered_task['project'], "random project")
120
121     def test_filter_for_task_without_space_in_project(self):
122         task = Task(self.tw, description="test", project="random")
123         task.save()
124
125         filtered_task = self.tw.tasks.get(project="random")
126         self.assertEqual(filtered_task['project'], "random")
127
128
129 class TaskTest(TasklibTest):
130
131     def test_create_unsaved_task(self):
132         # Make sure a new task is not saved unless explicitly called for
133         t = Task(self.tw, description="test task")
134         self.assertEqual(len(self.tw.tasks.all()), 0)
135
136     # TODO: once python 2.6 compatiblity is over, use context managers here
137     #       and in all subsequent tests for assertRaises
138
139     def test_delete_unsaved_task(self):
140         t = Task(self.tw, description="test task")
141         self.assertRaises(Task.NotSaved, t.delete)
142
143     def test_complete_unsaved_task(self):
144         t = Task(self.tw, description="test task")
145         self.assertRaises(Task.NotSaved, t.done)
146
147     def test_refresh_unsaved_task(self):
148         t = Task(self.tw, description="test task")
149         self.assertRaises(Task.NotSaved, t.refresh)
150
151     def test_delete_deleted_task(self):
152         t = Task(self.tw, description="test task")
153         t.save()
154         t.delete()
155
156         self.assertRaises(Task.DeletedTask, t.delete)
157
158     def test_complete_completed_task(self):
159         t = Task(self.tw, description="test task")
160         t.save()
161         t.done()
162
163         self.assertRaises(Task.CompletedTask, t.done)
164
165     def test_complete_deleted_task(self):
166         t = Task(self.tw, description="test task")
167         t.save()
168         t.delete()
169
170         self.assertRaises(Task.DeletedTask, t.done)
171
172     def test_modify_simple_attribute_without_space(self):
173         t = Task(self.tw, description="test")
174         t.save()
175
176         self.assertEquals(t['description'], "test")
177
178         t['description'] = "test-modified"
179         t.save()
180
181         self.assertEquals(t['description'], "test-modified")
182
183     def test_modify_simple_attribute_with_space(self):
184         # Space can pose problems with parsing
185         t = Task(self.tw, description="test task")
186         t.save()
187
188         self.assertEquals(t['description'], "test task")
189
190         t['description'] = "test task modified"
191         t.save()
192
193         self.assertEquals(t['description'], "test task modified")
194
195     def test_empty_dependency_set_of_unsaved_task(self):
196         t = Task(self.tw, description="test task")
197         self.assertEqual(t['depends'], set())
198
199     def test_empty_dependency_set_of_saved_task(self):
200         t = Task(self.tw, description="test task")
201         t.save()
202         self.assertEqual(t['depends'], set())
203
204     def test_set_unsaved_task_as_dependency(self):
205         # Adds only one dependency to task with no dependencies
206         t = Task(self.tw, description="test task")
207         dependency = Task(self.tw, description="needs to be done first")
208
209         # We only save the parent task, dependency task is unsaved
210         t.save()
211         t['depends'] = set([dependency])
212
213         self.assertRaises(Task.NotSaved, t.save)
214
215     def test_set_simple_dependency_set(self):
216         # Adds only one dependency to task with no dependencies
217         t = Task(self.tw, description="test task")
218         dependency = Task(self.tw, description="needs to be done first")
219
220         t.save()
221         dependency.save()
222
223         t['depends'] = set([dependency])
224
225         self.assertEqual(t['depends'], set([dependency]))
226
227     def test_set_complex_dependency_set(self):
228         # Adds two dependencies to task with no dependencies
229         t = Task(self.tw, description="test task")
230         dependency1 = Task(self.tw, description="needs to be done first")
231         dependency2 = Task(self.tw, description="needs to be done second")
232
233         t.save()
234         dependency1.save()
235         dependency2.save()
236
237         t['depends'] = set([dependency1, dependency2])
238
239         self.assertEqual(t['depends'], set([dependency1, dependency2]))
240
241     def test_remove_from_dependency_set(self):
242         # Removes dependency from task with two dependencies
243         t = Task(self.tw, description="test task")
244         dependency1 = Task(self.tw, description="needs to be done first")
245         dependency2 = Task(self.tw, description="needs to be done second")
246
247         dependency1.save()
248         dependency2.save()
249
250         t['depends'] = set([dependency1, dependency2])
251         t.save()
252
253         t['depends'].remove(dependency2)
254         t.save()
255
256         self.assertEqual(t['depends'], set([dependency1]))
257
258     def test_add_to_dependency_set(self):
259         # Adds dependency to task with one dependencies
260         t = Task(self.tw, description="test task")
261         dependency1 = Task(self.tw, description="needs to be done first")
262         dependency2 = Task(self.tw, description="needs to be done second")
263
264         dependency1.save()
265         dependency2.save()
266
267         t['depends'] = set([dependency1])
268         t.save()
269
270         t['depends'].add(dependency2)
271         t.save()
272
273         self.assertEqual(t['depends'], set([dependency1, dependency2]))
274
275     def test_add_to_empty_dependency_set(self):
276         # Adds dependency to task with one dependencies
277         t = Task(self.tw, description="test task")
278         dependency = Task(self.tw, description="needs to be done first")
279
280         dependency.save()
281
282         t['depends'].add(dependency)
283         t.save()
284
285         self.assertEqual(t['depends'], set([dependency]))
286
287     def test_simple_dependency_set_save_repeatedly(self):
288         # Adds only one dependency to task with no dependencies
289         t = Task(self.tw, description="test task")
290         dependency = Task(self.tw, description="needs to be done first")
291         dependency.save()
292
293         t['depends'] = set([dependency])
294         t.save()
295
296         # We taint the task, but keep depends intact
297         t['description'] = "test task modified"
298         t.save()
299
300         self.assertEqual(t['depends'], set([dependency]))
301
302         # We taint the task, but assign the same set to the depends
303         t['depends'] = set([dependency])
304         t['description'] = "test task modified again"
305         t.save()
306
307         self.assertEqual(t['depends'], set([dependency]))
308
309     def test_compare_different_tasks(self):
310         # Negative: compare two different tasks
311         t1 = Task(self.tw, description="test task")
312         t2 = Task(self.tw, description="test task")
313
314         t1.save()
315         t2.save()
316
317         self.assertEqual(t1 == t2, False)
318
319     def test_compare_same_task_object(self):
320         # Compare Task object wit itself
321         t = Task(self.tw, description="test task")
322         t.save()
323
324         self.assertEqual(t == t, True)
325
326     def test_compare_same_task(self):
327         # Compare the same task using two different objects
328         t1 = Task(self.tw, description="test task")
329         t1.save()
330
331         t2 = self.tw.tasks.get(uuid=t1['uuid'])
332         self.assertEqual(t1 == t2, True)
333
334     def test_compare_unsaved_tasks(self):
335         # t1 and t2 are unsaved tasks, considered to be unequal
336         # despite the content of data
337         t1 = Task(self.tw, description="test task")
338         t2 = Task(self.tw, description="test task")
339
340         self.assertEqual(t1 == t2, False)
341
342     def test_hash_unsaved_tasks(self):
343         # Considered equal, it's the same object
344         t1 = Task(self.tw, description="test task")
345         t2 = t1
346         self.assertEqual(hash(t1) == hash(t2), True)
347
348     def test_hash_same_task(self):
349         # Compare the hash of the task using two different objects
350         t1 = Task(self.tw, description="test task")
351         t1.save()
352
353         t2 = self.tw.tasks.get(uuid=t1['uuid'])
354         self.assertEqual(t1.__hash__(), t2.__hash__())
355
356     def test_adding_task_with_priority(self):
357         t = Task(self.tw, description="test task", priority="M")
358         t.save()
359
360     def test_removing_priority_with_none(self):
361         t = Task(self.tw, description="test task", priority="L")
362         t.save()
363
364         # Remove the priority mark
365         t['priority'] = None
366         t.save()
367
368         # Assert that priority is not there after saving
369         self.assertEqual(t['priority'], None)
370
371     def test_adding_task_with_due_time(self):
372         t = Task(self.tw, description="test task", due=datetime.datetime.now())
373         t.save()
374
375     def test_removing_due_time_with_none(self):
376         t = Task(self.tw, description="test task", due=datetime.datetime.now())
377         t.save()
378
379         # Remove the due timestamp
380         t['due'] = None
381         t.save()
382
383         # Assert that due timestamp is no longer there
384         self.assertEqual(t['due'], None)
385
386     def test_modified_fields_new_task(self):
387         t = Task(self.tw)
388
389         # This should be empty with new task
390         self.assertEqual(set(t._modified_fields), set())
391
392         # Modify the task
393         t['description'] = "test task"
394         self.assertEqual(set(t._modified_fields), set(['description']))
395
396         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
397         self.assertEqual(set(t._modified_fields), set(['description', 'due']))
398
399         t['project'] = "test project"
400         self.assertEqual(set(t._modified_fields),
401                          set(['description', 'due', 'project']))
402
403         # List of modified fields should clear out when saved
404         t.save()
405         self.assertEqual(set(t._modified_fields), set())
406
407         # Reassigning the fields with the same values now should not produce
408         # modified fields
409         t['description'] = "test task"
410         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
411         t['project'] = "test project"
412         self.assertEqual(set(t._modified_fields), set())
413
414     def test_modified_fields_loaded_task(self):
415         t = Task(self.tw)
416
417         # Modify the task
418         t['description'] = "test task"
419         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
420         t['project'] = "test project"
421
422         dependency = Task(self.tw, description="dependency")
423         dependency.save()
424         t['depends'] = set([dependency])
425
426         # List of modified fields should clear out when saved
427         t.save()
428         self.assertEqual(set(t._modified_fields), set())
429
430         # Get the task by using a filter by UUID
431         t2 = self.tw.tasks.get(uuid=t['uuid'])
432
433         # Reassigning the fields with the same values now should not produce
434         # modified fields
435         t['description'] = "test task"
436         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
437         t['project'] = "test project"
438         t['depends'] = set([dependency])
439         self.assertEqual(set(t._modified_fields), set())
440
441     def test_modified_fields_not_affected_by_reading(self):
442         t = Task(self.tw)
443
444         for field in TASK_STANDARD_ATTRS:
445             value = t[field]
446
447         self.assertEqual(set(t._modified_fields), set())
448
449     def test_setting_read_only_attrs_through_init(self):
450         # Test that we are unable to set readonly attrs through __init__
451         for readonly_key in Task.read_only_fields:
452             kwargs = {'description': 'test task', readonly_key: 'value'}
453             self.assertRaises(RuntimeError,
454                               lambda: Task(self.tw, **kwargs))
455
456     def test_setting_read_only_attrs_through_setitem(self):
457         # Test that we are unable to set readonly attrs through __init__
458         for readonly_key in Task.read_only_fields:
459             t = Task(self.tw, description='test task')
460             self.assertRaises(RuntimeError,
461                               lambda: t.__setitem__(readonly_key, 'value'))
462
463     def test_saving_unmodified_task(self):
464         t = Task(self.tw, description="test task")
465         t.save()
466         t.save()
467
468     def test_adding_tag_by_appending(self):
469         t = Task(self.tw, description="test task", tags=['test1'])
470         t.save()
471         t['tags'].append('test2')
472         t.save()
473         self.assertEqual(t['tags'], ['test1', 'test2'])
474
475     def test_adding_tag_by_appending_empty(self):
476         t = Task(self.tw, description="test task")
477         t.save()
478         t['tags'].append('test')
479         t.save()
480         self.assertEqual(t['tags'], ['test'])
481
482     def test_serializers_returning_empty_string_for_none(self):
483         # Test that any serializer returns '' when passed None
484         t = Task(self.tw)
485         serializers = [getattr(t, serializer_name) for serializer_name in
486                        filter(lambda x: x.startswith('serialize_'), dir(t))]
487         for serializer in serializers:
488             self.assertEqual(serializer(None), '')
489
490     def test_deserializer_returning_empty_value_for_empty_string(self):
491         # Test that any deserializer returns empty value when passed ''
492         t = Task(self.tw)
493         deserializers = [getattr(t, deserializer_name) for deserializer_name in
494                         filter(lambda x: x.startswith('deserialize_'), dir(t))]
495         for deserializer in deserializers:
496             self.assertTrue(deserializer('') in (None, [], set()))
497
498
499 class TaskFromHookTest(TasklibTest):
500
501     input_add_data = six.StringIO(
502         '{"description":"Buy some milk",'
503         '"entry":"20141118T050231Z",'
504         '"status":"pending",'
505         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
506
507     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
508         '{"description":"Buy some milk finally",'
509         '"entry":"20141118T050231Z",'
510         '"status":"completed",'
511         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
512
513     exported_raw_data = (
514         '{"project":"Home",'
515          '"due":"20150101T232323Z",'
516          '"description":"test task"}')
517
518     def test_setting_up_from_add_hook_input(self):
519         t = Task.from_input(input_file=self.input_add_data)
520         self.assertEqual(t['description'], "Buy some milk")
521         self.assertEqual(t.pending, True)
522
523     def test_setting_up_from_modified_hook_input(self):
524         t = Task.from_input(input_file=self.input_modify_data, modify=True)
525         self.assertEqual(t['description'], "Buy some milk finally")
526         self.assertEqual(t.pending, False)
527         self.assertEqual(t.completed, True)
528
529         self.assertEqual(t._original_data['status'], "pending")
530         self.assertEqual(t._original_data['description'], "Buy some milk")
531         self.assertEqual(set(t._modified_fields),
532                          set(['status', 'description']))
533
534     def test_export_data(self):
535         t = Task(self.tw, description="test task",
536             project="Home",
537             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
538
539         # Check that the output is a permutation of:
540         # {"project":"Home","description":"test task","due":"20150101232323Z"}
541         allowed_segments = self.exported_raw_data[1:-1].split(',')
542         allowed_output = [
543             '{' + ','.join(segments) + '}'
544             for segments in itertools.permutations(allowed_segments)
545         ]
546
547         self.assertTrue(any(t.export_data() == expected
548                             for expected in allowed_output))
549
550
551 class AnnotationTest(TasklibTest):
552
553     def setUp(self):
554         super(AnnotationTest, self).setUp()
555         Task(self.tw, description="test task").save()
556
557     def test_adding_annotation(self):
558         task = self.tw.tasks.get()
559         task.add_annotation('test annotation')
560         self.assertEqual(len(task['annotations']), 1)
561         ann = task['annotations'][0]
562         self.assertEqual(ann['description'], 'test annotation')
563
564     def test_removing_annotation(self):
565         task = self.tw.tasks.get()
566         task.add_annotation('test annotation')
567         ann = task['annotations'][0]
568         ann.remove()
569         self.assertEqual(len(task['annotations']), 0)
570
571     def test_removing_annotation_by_description(self):
572         task = self.tw.tasks.get()
573         task.add_annotation('test annotation')
574         task.remove_annotation('test annotation')
575         self.assertEqual(len(task['annotations']), 0)
576
577     def test_removing_annotation_by_obj(self):
578         task = self.tw.tasks.get()
579         task.add_annotation('test annotation')
580         ann = task['annotations'][0]
581         task.remove_annotation(ann)
582         self.assertEqual(len(task['annotations']), 0)
583
584     def test_annotation_after_modification(self):
585          task = self.tw.tasks.get()
586          task['project'] = 'test'
587          task.add_annotation('I should really do this task')
588          self.assertEqual(task['project'], 'test')
589          task.save()
590          self.assertEqual(task['project'], 'test')
591
592     def test_serialize_annotations(self):
593         # Test that serializing annotations is possible
594         t = Task(self.tw, description="test")
595         t.save()
596
597         t.add_annotation("annotation1")
598         t.add_annotation("annotation2")
599
600         data = t._serialize('annotations', t._data['annotations'])
601
602         self.assertEqual(len(data), 2)
603         self.assertEqual(type(data[0]), dict)
604         self.assertEqual(type(data[1]), dict)
605
606         self.assertEqual(data[0]['description'], "annotation1")
607         self.assertEqual(data[1]['description'], "annotation2")
608
609
610 class UnicodeTest(TasklibTest):
611
612     def test_unicode_task(self):
613         Task(self.tw, description="†åßk").save()
614         self.tw.tasks.get()
615
616     def test_non_unicode_task(self):
617         Task(self.tw, description="test task").save()
618         self.tw.tasks.get()