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

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