]> 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 test for filtering with unicode strings
[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_complete_deleted_task(self):
288         t = Task(self.tw, description="test task")
289         t.save()
290         t.delete()
291
292         self.assertRaises(Task.DeletedTask, t.done)
293
294     def test_starting_task(self):
295         t = Task(self.tw, description="test task")
296         now = t.datetime_normalizer(datetime.datetime.now())
297         t.save()
298         t.start()
299
300         self.assertTrue(now.replace(microsecond=0) <= t['start'])
301         self.assertEqual(t['status'], 'pending')
302
303     def test_completing_task(self):
304         t = Task(self.tw, description="test task")
305         now = t.datetime_normalizer(datetime.datetime.now())
306         t.save()
307         t.done()
308
309         self.assertTrue(now.replace(microsecond=0) <= t['end'])
310         self.assertEqual(t['status'], 'completed')
311
312     def test_deleting_task(self):
313         t = Task(self.tw, description="test task")
314         now = t.datetime_normalizer(datetime.datetime.now())
315         t.save()
316         t.delete()
317
318         self.assertTrue(now.replace(microsecond=0) <= t['end'])
319         self.assertEqual(t['status'], 'deleted')
320
321     def test_started_task_active(self):
322         t = Task(self.tw, description="test task")
323         t.save()
324         t.start()
325         self.assertTrue(t.active)
326
327     def test_unstarted_task_inactive(self):
328         t = Task(self.tw, description="test task")
329         self.assertFalse(t.active)
330         t.save()
331         self.assertFalse(t.active)
332
333     def test_modify_simple_attribute_without_space(self):
334         t = Task(self.tw, description="test")
335         t.save()
336
337         self.assertEquals(t['description'], "test")
338
339         t['description'] = "test-modified"
340         t.save()
341
342         self.assertEquals(t['description'], "test-modified")
343
344     def test_modify_simple_attribute_with_space(self):
345         # Space can pose problems with parsing
346         t = Task(self.tw, description="test task")
347         t.save()
348
349         self.assertEquals(t['description'], "test task")
350
351         t['description'] = "test task modified"
352         t.save()
353
354         self.assertEquals(t['description'], "test task modified")
355
356     def test_empty_dependency_set_of_unsaved_task(self):
357         t = Task(self.tw, description="test task")
358         self.assertEqual(t['depends'], set())
359
360     def test_empty_dependency_set_of_saved_task(self):
361         t = Task(self.tw, description="test task")
362         t.save()
363         self.assertEqual(t['depends'], set())
364
365     def test_set_unsaved_task_as_dependency(self):
366         # Adds only one dependency to task with no dependencies
367         t = Task(self.tw, description="test task")
368         dependency = Task(self.tw, description="needs to be done first")
369
370         # We only save the parent task, dependency task is unsaved
371         t.save()
372         t['depends'] = set([dependency])
373
374         self.assertRaises(Task.NotSaved, t.save)
375
376     def test_set_simple_dependency_set(self):
377         # Adds only one dependency to task with no dependencies
378         t = Task(self.tw, description="test task")
379         dependency = Task(self.tw, description="needs to be done first")
380
381         t.save()
382         dependency.save()
383
384         t['depends'] = set([dependency])
385
386         self.assertEqual(t['depends'], set([dependency]))
387
388     def test_set_complex_dependency_set(self):
389         # Adds two dependencies to task with no dependencies
390         t = Task(self.tw, description="test task")
391         dependency1 = Task(self.tw, description="needs to be done first")
392         dependency2 = Task(self.tw, description="needs to be done second")
393
394         t.save()
395         dependency1.save()
396         dependency2.save()
397
398         t['depends'] = set([dependency1, dependency2])
399
400         self.assertEqual(t['depends'], set([dependency1, dependency2]))
401
402     def test_remove_from_dependency_set(self):
403         # Removes dependency from task with two dependencies
404         t = Task(self.tw, description="test task")
405         dependency1 = Task(self.tw, description="needs to be done first")
406         dependency2 = Task(self.tw, description="needs to be done second")
407
408         dependency1.save()
409         dependency2.save()
410
411         t['depends'] = set([dependency1, dependency2])
412         t.save()
413
414         t['depends'].remove(dependency2)
415         t.save()
416
417         self.assertEqual(t['depends'], set([dependency1]))
418
419     def test_add_to_dependency_set(self):
420         # Adds dependency to task with one dependencies
421         t = Task(self.tw, description="test task")
422         dependency1 = Task(self.tw, description="needs to be done first")
423         dependency2 = Task(self.tw, description="needs to be done second")
424
425         dependency1.save()
426         dependency2.save()
427
428         t['depends'] = set([dependency1])
429         t.save()
430
431         t['depends'].add(dependency2)
432         t.save()
433
434         self.assertEqual(t['depends'], set([dependency1, dependency2]))
435
436     def test_add_to_empty_dependency_set(self):
437         # Adds dependency to task with one dependencies
438         t = Task(self.tw, description="test task")
439         dependency = Task(self.tw, description="needs to be done first")
440
441         dependency.save()
442
443         t['depends'].add(dependency)
444         t.save()
445
446         self.assertEqual(t['depends'], set([dependency]))
447
448     def test_simple_dependency_set_save_repeatedly(self):
449         # Adds only one dependency to task with no dependencies
450         t = Task(self.tw, description="test task")
451         dependency = Task(self.tw, description="needs to be done first")
452         dependency.save()
453
454         t['depends'] = set([dependency])
455         t.save()
456
457         # We taint the task, but keep depends intact
458         t['description'] = "test task modified"
459         t.save()
460
461         self.assertEqual(t['depends'], set([dependency]))
462
463         # We taint the task, but assign the same set to the depends
464         t['depends'] = set([dependency])
465         t['description'] = "test task modified again"
466         t.save()
467
468         self.assertEqual(t['depends'], set([dependency]))
469
470     def test_compare_different_tasks(self):
471         # Negative: compare two different tasks
472         t1 = Task(self.tw, description="test task")
473         t2 = Task(self.tw, description="test task")
474
475         t1.save()
476         t2.save()
477
478         self.assertEqual(t1 == t2, False)
479
480     def test_compare_same_task_object(self):
481         # Compare Task object wit itself
482         t = Task(self.tw, description="test task")
483         t.save()
484
485         self.assertEqual(t == t, True)
486
487     def test_compare_same_task(self):
488         # Compare the same task using two different objects
489         t1 = Task(self.tw, description="test task")
490         t1.save()
491
492         t2 = self.tw.tasks.get(uuid=t1['uuid'])
493         self.assertEqual(t1 == t2, True)
494
495     def test_compare_unsaved_tasks(self):
496         # t1 and t2 are unsaved tasks, considered to be unequal
497         # despite the content of data
498         t1 = Task(self.tw, description="test task")
499         t2 = Task(self.tw, description="test task")
500
501         self.assertEqual(t1 == t2, False)
502
503     def test_hash_unsaved_tasks(self):
504         # Considered equal, it's the same object
505         t1 = Task(self.tw, description="test task")
506         t2 = t1
507         self.assertEqual(hash(t1) == hash(t2), True)
508
509     def test_hash_same_task(self):
510         # Compare the hash of the task using two different objects
511         t1 = Task(self.tw, description="test task")
512         t1.save()
513
514         t2 = self.tw.tasks.get(uuid=t1['uuid'])
515         self.assertEqual(t1.__hash__(), t2.__hash__())
516
517     def test_adding_task_with_priority(self):
518         t = Task(self.tw, description="test task", priority="M")
519         t.save()
520
521     def test_removing_priority_with_none(self):
522         t = Task(self.tw, description="test task", priority="L")
523         t.save()
524
525         # Remove the priority mark
526         t['priority'] = None
527         t.save()
528
529         # Assert that priority is not there after saving
530         self.assertEqual(t['priority'], None)
531
532     def test_adding_task_with_due_time(self):
533         t = Task(self.tw, description="test task", due=datetime.datetime.now())
534         t.save()
535
536     def test_removing_due_time_with_none(self):
537         t = Task(self.tw, description="test task", due=datetime.datetime.now())
538         t.save()
539
540         # Remove the due timestamp
541         t['due'] = None
542         t.save()
543
544         # Assert that due timestamp is no longer there
545         self.assertEqual(t['due'], None)
546
547     def test_modified_fields_new_task(self):
548         t = Task(self.tw)
549
550         # This should be empty with new task
551         self.assertEqual(set(t._modified_fields), set())
552
553         # Modify the task
554         t['description'] = "test task"
555         self.assertEqual(set(t._modified_fields), set(['description']))
556
557         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
558         self.assertEqual(set(t._modified_fields), set(['description', 'due']))
559
560         t['project'] = "test project"
561         self.assertEqual(set(t._modified_fields),
562                          set(['description', 'due', 'project']))
563
564         # List of modified fields should clear out when saved
565         t.save()
566         self.assertEqual(set(t._modified_fields), set())
567
568         # Reassigning the fields with the same values now should not produce
569         # modified fields
570         t['description'] = "test task"
571         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
572         t['project'] = "test project"
573         self.assertEqual(set(t._modified_fields), set())
574
575     def test_modified_fields_loaded_task(self):
576         t = Task(self.tw)
577
578         # Modify the task
579         t['description'] = "test task"
580         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
581         t['project'] = "test project"
582
583         dependency = Task(self.tw, description="dependency")
584         dependency.save()
585         t['depends'] = set([dependency])
586
587         # List of modified fields should clear out when saved
588         t.save()
589         self.assertEqual(set(t._modified_fields), set())
590
591         # Get the task by using a filter by UUID
592         t2 = self.tw.tasks.get(uuid=t['uuid'])
593
594         # Reassigning the fields with the same values now should not produce
595         # modified fields
596         t['description'] = "test task"
597         t['due'] = datetime.datetime(2014, 2, 14, 14, 14, 14)  # <3
598         t['project'] = "test project"
599         t['depends'] = set([dependency])
600         self.assertEqual(set(t._modified_fields), set())
601
602     def test_modified_fields_not_affected_by_reading(self):
603         t = Task(self.tw)
604
605         for field in TASK_STANDARD_ATTRS:
606             value = t[field]
607
608         self.assertEqual(set(t._modified_fields), set())
609
610     def test_setting_read_only_attrs_through_init(self):
611         # Test that we are unable to set readonly attrs through __init__
612         for readonly_key in Task.read_only_fields:
613             kwargs = {'description': 'test task', readonly_key: 'value'}
614             self.assertRaises(RuntimeError,
615                               lambda: Task(self.tw, **kwargs))
616
617     def test_setting_read_only_attrs_through_setitem(self):
618         # Test that we are unable to set readonly attrs through __init__
619         for readonly_key in Task.read_only_fields:
620             t = Task(self.tw, description='test task')
621             self.assertRaises(RuntimeError,
622                               lambda: t.__setitem__(readonly_key, 'value'))
623
624     def test_saving_unmodified_task(self):
625         t = Task(self.tw, description="test task")
626         t.save()
627         t.save()
628
629     def test_adding_tag_by_appending(self):
630         t = Task(self.tw, description="test task", tags=['test1'])
631         t.save()
632         t['tags'].append('test2')
633         t.save()
634         self.assertEqual(t['tags'], ['test1', 'test2'])
635
636     def test_adding_tag_by_appending_empty(self):
637         t = Task(self.tw, description="test task")
638         t.save()
639         t['tags'].append('test')
640         t.save()
641         self.assertEqual(t['tags'], ['test'])
642
643     def test_serializers_returning_empty_string_for_none(self):
644         # Test that any serializer returns '' when passed None
645         t = Task(self.tw)
646         serializers = [getattr(t, serializer_name) for serializer_name in
647                        filter(lambda x: x.startswith('serialize_'), dir(t))]
648         for serializer in serializers:
649             self.assertEqual(serializer(None), '')
650
651     def test_deserializer_returning_empty_value_for_empty_string(self):
652         # Test that any deserializer returns empty value when passed ''
653         t = Task(self.tw)
654         deserializers = [getattr(t, deserializer_name) for deserializer_name in
655                         filter(lambda x: x.startswith('deserialize_'), dir(t))]
656         for deserializer in deserializers:
657             self.assertTrue(deserializer('') in (None, [], set()))
658
659     def test_normalizers_handling_none(self):
660         # Test that any normalizer can handle None as a valid value
661         t = Task(self.tw)
662
663         for key in TASK_STANDARD_ATTRS:
664             t._normalize(key, None)
665
666     def test_recurrent_task_generation(self):
667         today = datetime.date.today()
668         t = Task(self.tw, description="brush teeth",
669                  due=today, recur="daily")
670         t.save()
671         self.assertEqual(len(self.tw.tasks.pending()), 2)
672
673     def test_modify_number_of_tasks_at_once(self):
674         for i in range(1, 100):
675             Task(self.tw, description="test task %d" % i, tags=['test']).save()
676
677         self.tw.execute_command(['+test', 'mod', 'unified', 'description'])
678
679
680 class TaskFromHookTest(TasklibTest):
681
682     input_add_data = six.StringIO(
683         '{"description":"Buy some milk",'
684         '"entry":"20141118T050231Z",'
685         '"status":"pending",'
686         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
687
688     input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' +
689         '{"description":"Buy some milk finally",'
690         '"entry":"20141118T050231Z",'
691         '"status":"completed",'
692         '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}')
693
694     exported_raw_data = (
695         '{"project":"Home",'
696          '"due":"20150101T232323Z",'
697          '"description":"test task"}')
698
699     def test_setting_up_from_add_hook_input(self):
700         t = Task.from_input(input_file=self.input_add_data, warrior=self.tw)
701         self.assertEqual(t['description'], "Buy some milk")
702         self.assertEqual(t.pending, True)
703
704     def test_setting_up_from_modified_hook_input(self):
705         t = Task.from_input(input_file=self.input_modify_data, modify=True,
706                             warrior=self.tw)
707         self.assertEqual(t['description'], "Buy some milk finally")
708         self.assertEqual(t.pending, False)
709         self.assertEqual(t.completed, True)
710
711         self.assertEqual(t._original_data['status'], "pending")
712         self.assertEqual(t._original_data['description'], "Buy some milk")
713         self.assertEqual(set(t._modified_fields),
714                          set(['status', 'description']))
715
716     def test_export_data(self):
717         t = Task(self.tw, description="test task",
718             project="Home",
719             due=pytz.utc.localize(datetime.datetime(2015,1,1,23,23,23)))
720
721         # Check that the output is a permutation of:
722         # {"project":"Home","description":"test task","due":"20150101232323Z"}
723         allowed_segments = self.exported_raw_data[1:-1].split(',')
724         allowed_output = [
725             '{' + ','.join(segments) + '}'
726             for segments in itertools.permutations(allowed_segments)
727         ]
728
729         self.assertTrue(any(t.export_data() == expected
730                             for expected in allowed_output))
731
732 class TimezoneAwareDatetimeTest(TasklibTest):
733
734     def setUp(self):
735         super(TimezoneAwareDatetimeTest, self).setUp()
736         self.zone = local_zone
737         self.localdate_naive = datetime.datetime(2015,2,2)
738         self.localtime_naive = datetime.datetime(2015,2,2,0,0,0)
739         self.localtime_aware = self.zone.localize(self.localtime_naive)
740         self.utctime_aware = self.localtime_aware.astimezone(pytz.utc)
741
742     def test_timezone_naive_datetime_setitem(self):
743         t = Task(self.tw, description="test task")
744         t['due'] = self.localtime_naive
745         self.assertEqual(t['due'], self.localtime_aware)
746
747     def test_timezone_naive_datetime_using_init(self):
748         t = Task(self.tw, description="test task", due=self.localtime_naive)
749         self.assertEqual(t['due'], self.localtime_aware)
750
751     def test_filter_by_naive_datetime(self):
752         t = Task(self.tw, description="task1", due=self.localtime_naive)
753         t.save()
754         matching_tasks = self.tw.tasks.filter(due=self.localtime_naive)
755         self.assertEqual(len(matching_tasks), 1)
756
757     def test_serialize_naive_datetime(self):
758         t = Task(self.tw, description="task1", due=self.localtime_naive)
759         self.assertEqual(json.loads(t.export_data())['due'],
760                          self.utctime_aware.strftime(DATE_FORMAT))
761
762     def test_timezone_naive_date_setitem(self):
763         t = Task(self.tw, description="test task")
764         t['due'] = self.localdate_naive
765         self.assertEqual(t['due'], self.localtime_aware)
766
767     def test_timezone_naive_date_using_init(self):
768         t = Task(self.tw, description="test task", due=self.localdate_naive)
769         self.assertEqual(t['due'], self.localtime_aware)
770
771     def test_filter_by_naive_date(self):
772         t = Task(self.tw, description="task1", due=self.localdate_naive)
773         t.save()
774         matching_tasks = self.tw.tasks.filter(due=self.localdate_naive)
775         self.assertEqual(len(matching_tasks), 1)
776
777     def test_serialize_naive_date(self):
778         t = Task(self.tw, description="task1", due=self.localdate_naive)
779         self.assertEqual(json.loads(t.export_data())['due'],
780                          self.utctime_aware.strftime(DATE_FORMAT))
781
782     def test_timezone_aware_datetime_setitem(self):
783         t = Task(self.tw, description="test task")
784         t['due'] = self.localtime_aware
785         self.assertEqual(t['due'], self.localtime_aware)
786
787     def test_timezone_aware_datetime_using_init(self):
788         t = Task(self.tw, description="test task", due=self.localtime_aware)
789         self.assertEqual(t['due'], self.localtime_aware)
790
791     def test_filter_by_aware_datetime(self):
792         t = Task(self.tw, description="task1", due=self.localtime_aware)
793         t.save()
794         matching_tasks = self.tw.tasks.filter(due=self.localtime_aware)
795         self.assertEqual(len(matching_tasks), 1)
796
797     def test_serialize_aware_datetime(self):
798         t = Task(self.tw, description="task1", due=self.localtime_aware)
799         self.assertEqual(json.loads(t.export_data())['due'],
800                          self.utctime_aware.strftime(DATE_FORMAT))
801
802 class DatetimeStringTest(TasklibTest):
803
804     def test_simple_now_conversion(self):
805         if self.tw.version < six.text_type('2.4.0'):
806             # Python2.6 does not support SkipTest. As a workaround
807             # mark the test as passed by exiting.
808             if getattr(unittest, 'SkipTest', None) is not None:
809                 raise unittest.SkipTest()
810             else:
811                 return
812
813         t = Task(self.tw, description="test task", due="now")
814         now = local_zone.localize(datetime.datetime.now())
815
816         # Assert that both times are not more than 5 seconds apart
817         if sys.version_info < (2,7):
818             self.assertTrue(total_seconds_2_6(now - t['due']) < 5)
819             self.assertTrue(total_seconds_2_6(t['due'] - now) < 5)
820         else:
821             self.assertTrue((now - t['due']).total_seconds() < 5)
822             self.assertTrue((t['due'] - now).total_seconds() < 5)
823
824     def test_simple_eoy_conversion(self):
825         if self.tw.version < six.text_type('2.4.0'):
826             # Python2.6 does not support SkipTest. As a workaround
827             # mark the test as passed by exiting.
828             if getattr(unittest, 'SkipTest', None) is not None:
829                 raise unittest.SkipTest()
830             else:
831                 return
832
833         t = Task(self.tw, description="test task", due="eoy")
834         now = local_zone.localize(datetime.datetime.now())
835         eoy = local_zone.localize(datetime.datetime(
836             year=now.year,
837             month=12,
838             day=31,
839             hour=23,
840             minute=59,
841             second=59
842             ))
843         self.assertEqual(eoy, t['due'])
844
845     def test_complex_eoy_conversion(self):
846         if self.tw.version < six.text_type('2.4.0'):
847             # Python2.6 does not support SkipTest. As a workaround
848             # mark the test as passed by exiting.
849             if getattr(unittest, 'SkipTest', None) is not None:
850                 raise unittest.SkipTest()
851             else:
852                 return
853
854         t = Task(self.tw, description="test task", due="eoy - 4 months")
855         now = local_zone.localize(datetime.datetime.now())
856         due_date = local_zone.localize(datetime.datetime(
857             year=now.year,
858             month=12,
859             day=31,
860             hour=23,
861             minute=59,
862             second=59
863             )) - datetime.timedelta(0,4 * 30 * 86400)
864         self.assertEqual(due_date, t['due'])
865
866     def test_filtering_with_string_datetime(self):
867         if self.tw.version < six.text_type('2.4.0'):
868             # Python2.6 does not support SkipTest. As a workaround
869             # mark the test as passed by exiting.
870             if getattr(unittest, 'SkipTest', None) is not None:
871                 raise unittest.SkipTest()
872             else:
873                 return
874
875         t = Task(self.tw, description="test task",
876                  due=datetime.datetime.now() - datetime.timedelta(0,2))
877         t.save()
878         self.assertEqual(len(self.tw.tasks.filter(due__before="now")), 1)
879
880 class AnnotationTest(TasklibTest):
881
882     def setUp(self):
883         super(AnnotationTest, self).setUp()
884         Task(self.tw, description="test task").save()
885
886     def test_adding_annotation(self):
887         task = self.tw.tasks.get()
888         task.add_annotation('test annotation')
889         self.assertEqual(len(task['annotations']), 1)
890         ann = task['annotations'][0]
891         self.assertEqual(ann['description'], 'test annotation')
892
893     def test_removing_annotation(self):
894         task = self.tw.tasks.get()
895         task.add_annotation('test annotation')
896         ann = task['annotations'][0]
897         ann.remove()
898         self.assertEqual(len(task['annotations']), 0)
899
900     def test_removing_annotation_by_description(self):
901         task = self.tw.tasks.get()
902         task.add_annotation('test annotation')
903         task.remove_annotation('test annotation')
904         self.assertEqual(len(task['annotations']), 0)
905
906     def test_removing_annotation_by_obj(self):
907         task = self.tw.tasks.get()
908         task.add_annotation('test annotation')
909         ann = task['annotations'][0]
910         task.remove_annotation(ann)
911         self.assertEqual(len(task['annotations']), 0)
912
913     def test_annotation_after_modification(self):
914          task = self.tw.tasks.get()
915          task['project'] = 'test'
916          task.add_annotation('I should really do this task')
917          self.assertEqual(task['project'], 'test')
918          task.save()
919          self.assertEqual(task['project'], 'test')
920
921     def test_serialize_annotations(self):
922         # Test that serializing annotations is possible
923         t = Task(self.tw, description="test")
924         t.save()
925
926         t.add_annotation("annotation1")
927         t.add_annotation("annotation2")
928
929         data = t._serialize('annotations', t._data['annotations'])
930
931         self.assertEqual(len(data), 2)
932         self.assertEqual(type(data[0]), dict)
933         self.assertEqual(type(data[1]), dict)
934
935         self.assertEqual(data[0]['description'], "annotation1")
936         self.assertEqual(data[1]['description'], "annotation2")
937
938
939 class UnicodeTest(TasklibTest):
940
941     def test_unicode_task(self):
942         Task(self.tw, description="†åßk").save()
943         self.tw.tasks.get()
944
945     def test_filter_by_unicode_task(self):
946         Task(self.tw, description="†åßk").save()
947         tasks = self.tw.tasks.filter(description="†åßk")
948         self.assertEqual(len(tasks), 1)
949
950     def test_non_unicode_task(self):
951         Task(self.tw, description="test task").save()
952         self.tw.tasks.get()
953
954 class ReadOnlyDictViewTest(unittest.TestCase):
955
956     def setUp(self):
957         self.sample = dict(l=[1,2,3], d={'k':'v'})
958         self.original_sample = copy.deepcopy(self.sample)
959         self.view = ReadOnlyDictView(self.sample)
960
961     def test_readonlydictview_getitem(self):
962         l = self.view['l']
963         self.assertEqual(l, self.sample['l'])
964
965         # Assert that modification changed only copied value
966         l.append(4)
967         self.assertNotEqual(l, self.sample['l'])
968
969         # Assert that viewed dict is not changed
970         self.assertEqual(self.sample, self.original_sample)
971
972     def test_readonlydictview_contains(self):
973         self.assertEqual('l' in self.view, 'l' in self.sample)
974         self.assertEqual('d' in self.view, 'd' in self.sample)
975         self.assertEqual('k' in self.view, 'k' in self.sample)
976
977         # Assert that viewed dict is not changed
978         self.assertEqual(self.sample, self.original_sample)
979
980     def test_readonlydictview_iter(self):
981         self.assertEqual(list(k for k in self.view),
982                          list(k for k in self.sample))
983
984         # Assert the view is correct after modification
985         self.sample['new'] = 'value'
986         self.assertEqual(list(k for k in self.view),
987                          list(k for k in self.sample))
988
989     def test_readonlydictview_len(self):
990         self.assertEqual(len(self.view), len(self.sample))
991
992         # Assert the view is correct after modification
993         self.sample['new'] = 'value'
994         self.assertEqual(len(self.view), len(self.sample))
995
996     def test_readonlydictview_get(self):
997         l = self.view.get('l')
998         self.assertEqual(l, self.sample.get('l'))
999
1000         # Assert that modification changed only copied value
1001         l.append(4)
1002         self.assertNotEqual(l, self.sample.get('l'))
1003
1004         # Assert that viewed dict is not changed
1005         self.assertEqual(self.sample, self.original_sample)
1006
1007     def test_readonlydict_items(self):
1008         view_items = self.view.items()
1009         sample_items = list(self.sample.items())
1010         self.assertEqual(view_items, sample_items)
1011
1012         view_items.append('newkey')
1013         self.assertNotEqual(view_items, sample_items)
1014         self.assertEqual(self.sample, self.original_sample)
1015
1016     def test_readonlydict_values(self):
1017         view_values = self.view.values()
1018         sample_values = list(self.sample.values())
1019         self.assertEqual(view_values, sample_values)
1020
1021         view_list_item = list(filter(lambda x: type(x) is list,
1022                                      view_values))[0]
1023         view_list_item.append(4)
1024         self.assertNotEqual(view_values, sample_values)
1025         self.assertEqual(self.sample, self.original_sample)