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

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