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

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