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

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