X-Git-Url: https://git.madduck.net/etc/taskwarrior.git/blobdiff_plain/27f93e251029f1d93c92d8e1ac6a254bdb70ecdd..578adfd8dd1bda7bbf535719371381f9a3794a1e:/tasklib/tests.py?ds=sidebyside diff --git a/tasklib/tests.py b/tasklib/tests.py index 3ff1ebf..b0e342d 100644 --- a/tasklib/tests.py +++ b/tasklib/tests.py @@ -1,15 +1,20 @@ # coding=utf-8 +import copy import datetime import itertools import json import pytz import six import shutil +import sys import tempfile import unittest -from .task import TaskWarrior, Task, local_zone, DATE_FORMAT +from .backends import TaskWarrior +from .task import Task, ReadOnlyDictView, TaskQuerySet +from .lazy import LazyUUIDTask, LazyUUIDTaskSet +from .serializing import DATE_FORMAT, local_zone # http://taskwarrior.org/docs/design/task.html , Section: The Attributes TASK_STANDARD_ATTRS = ( @@ -35,11 +40,14 @@ TASK_STANDARD_ATTRS = ( 'annotations', ) +total_seconds_2_6 = lambda x: x.microseconds / 1e6 + x.seconds + x.days * 24 * 3600 + + class TasklibTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.mkdtemp(dir='.') - self.tw = TaskWarrior(data_location=self.tmp) + self.tw = TaskWarrior(data_location=self.tmp, taskrc_location='/') def tearDown(self): shutil.rmtree(self.tmp) @@ -125,6 +133,112 @@ class TaskFilterTest(TasklibTest): filtered_task = self.tw.tasks.get(project="random") self.assertEqual(filtered_task['project'], "random") + def test_filter_with_empty_uuid(self): + self.assertRaises(ValueError, lambda: self.tw.tasks.get(uuid='')) + + def test_filter_dummy_by_status(self): + t = Task(self.tw, description="test") + t.save() + + tasks = self.tw.tasks.filter(status=t['status']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_uuid(self): + t = Task(self.tw, description="test") + t.save() + + tasks = self.tw.tasks.filter(uuid=t['uuid']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_entry(self): + t = Task(self.tw, description="test") + t.save() + + tasks = self.tw.tasks.filter(entry=t['entry']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_description(self): + t = Task(self.tw, description="test") + t.save() + + tasks = self.tw.tasks.filter(description=t['description']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_start(self): + t = Task(self.tw, description="test") + t.save() + t.start() + + tasks = self.tw.tasks.filter(start=t['start']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_end(self): + t = Task(self.tw, description="test") + t.save() + t.done() + + tasks = self.tw.tasks.filter(end=t['end']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_due(self): + t = Task(self.tw, description="test", due=datetime.datetime.now()) + t.save() + + tasks = self.tw.tasks.filter(due=t['due']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_until(self): + t = Task(self.tw, description="test") + t.save() + + tasks = self.tw.tasks.filter(until=t['until']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_modified(self): + # Older TW version does not support bumping modified + # on save + if self.tw.version < six.text_type('2.2.0'): + # Python2.6 does not support SkipTest. As a workaround + # mark the test as passed by exiting. + if getattr(unittest, 'SkipTest', None) is not None: + raise unittest.SkipTest() + else: + return + + t = Task(self.tw, description="test") + t.save() + + tasks = self.tw.tasks.filter(modified=t['modified']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_scheduled(self): + t = Task(self.tw, description="test") + t.save() + + tasks = self.tw.tasks.filter(scheduled=t['scheduled']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_tags(self): + t = Task(self.tw, description="test", tags=["home"]) + t.save() + + tasks = self.tw.tasks.filter(tags=t['tags']) + self.assertEqual(list(tasks), [t]) + + def test_filter_dummy_by_projects(self): + t = Task(self.tw, description="test", project="random") + t.save() + + tasks = self.tw.tasks.filter(project=t['project']) + self.assertEqual(list(tasks), [t]) + + def test_filter_by_priority(self): + t = Task(self.tw, description="test", priority="H") + t.save() + + tasks = self.tw.tasks.filter(priority=t['priority']) + self.assertEqual(list(tasks), [t]) + class TaskTest(TasklibTest): @@ -148,6 +262,10 @@ class TaskTest(TasklibTest): t = Task(self.tw, description="test task") self.assertRaises(Task.NotSaved, t.refresh) + def test_start_unsaved_task(self): + t = Task(self.tw, description="test task") + self.assertRaises(Task.NotSaved, t.start) + def test_delete_deleted_task(self): t = Task(self.tw, description="test task") t.save() @@ -162,6 +280,26 @@ class TaskTest(TasklibTest): self.assertRaises(Task.CompletedTask, t.done) + def test_start_completed_task(self): + t = Task(self.tw, description="test task") + t.save() + t.done() + + self.assertRaises(Task.CompletedTask, t.start) + + def test_add_completed_task(self): + t = Task(self.tw, description="test", status="completed", + end=datetime.datetime.now()) + t.save() + + def test_add_multiple_completed_tasks(self): + t1 = Task(self.tw, description="test1", status="completed", + end=datetime.datetime.now()) + t2 = Task(self.tw, description="test2", status="completed", + end=datetime.datetime.now()) + t1.save() + t2.save() + def test_complete_deleted_task(self): t = Task(self.tw, description="test task") t.save() @@ -169,6 +307,96 @@ class TaskTest(TasklibTest): self.assertRaises(Task.DeletedTask, t.done) + def test_starting_task(self): + t = Task(self.tw, description="test task") + now = t.datetime_normalizer(datetime.datetime.now()) + t.save() + t.start() + + self.assertTrue(now.replace(microsecond=0) <= t['start']) + self.assertEqual(t['status'], 'pending') + + def test_completing_task(self): + t = Task(self.tw, description="test task") + now = t.datetime_normalizer(datetime.datetime.now()) + t.save() + t.done() + + self.assertTrue(now.replace(microsecond=0) <= t['end']) + self.assertEqual(t['status'], 'completed') + + def test_deleting_task(self): + t = Task(self.tw, description="test task") + now = t.datetime_normalizer(datetime.datetime.now()) + t.save() + t.delete() + + self.assertTrue(now.replace(microsecond=0) <= t['end']) + self.assertEqual(t['status'], 'deleted') + + def test_started_task_active(self): + t = Task(self.tw, description="test task") + t.save() + t.start() + self.assertTrue(t.active) + + def test_unstarted_task_inactive(self): + t = Task(self.tw, description="test task") + self.assertFalse(t.active) + t.save() + self.assertFalse(t.active) + + def test_start_active_task(self): + t = Task(self.tw, description="test task") + t.save() + t.start() + self.assertRaises(Task.ActiveTask, t.start) + + def test_stop_completed_task(self): + t = Task(self.tw, description="test task") + t.save() + t.start() + t.done() + + self.assertRaises(Task.InactiveTask, t.stop) + + t = Task(self.tw, description="test task") + t.save() + t.done() + + self.assertRaises(Task.InactiveTask, t.stop) + + def test_stop_deleted_task(self): + t = Task(self.tw, description="test task") + t.save() + t.start() + t.delete() + t.stop() + + def test_stop_inactive_task(self): + t = Task(self.tw, description="test task") + t.save() + + self.assertRaises(Task.InactiveTask, t.stop) + + t = Task(self.tw, description="test task") + t.save() + t.start() + t.stop() + + self.assertRaises(Task.InactiveTask, t.stop) + + def test_stopping_task(self): + t = Task(self.tw, description="test task") + now = t.datetime_normalizer(datetime.datetime.now()) + t.save() + t.start() + t.stop() + + self.assertEqual(t['end'], None) + self.assertEqual(t['status'], 'pending') + self.assertFalse(t.active) + def test_modify_simple_attribute_without_space(self): t = Task(self.tw, description="test") t.save() @@ -468,16 +696,24 @@ class TaskTest(TasklibTest): def test_adding_tag_by_appending(self): t = Task(self.tw, description="test task", tags=['test1']) t.save() - t['tags'].append('test2') + t['tags'].add('test2') t.save() - self.assertEqual(t['tags'], ['test1', 'test2']) + self.assertEqual(t['tags'], set(['test1', 'test2'])) + + def test_adding_tag_twice(self): + t = Task(self.tw, description="test task", tags=['test1']) + t.save() + t['tags'].add('test2') + t['tags'].add('test2') + t.save() + self.assertEqual(t['tags'], set(['test1', 'test2'])) def test_adding_tag_by_appending_empty(self): t = Task(self.tw, description="test task") t.save() - t['tags'].append('test') + t['tags'].add('test') t.save() - self.assertEqual(t['tags'], ['test']) + self.assertEqual(t['tags'], set(['test'])) def test_serializers_returning_empty_string_for_none(self): # Test that any serializer returns '' when passed None @@ -495,13 +731,12 @@ class TaskTest(TasklibTest): for deserializer in deserializers: self.assertTrue(deserializer('') in (None, [], set())) - def test_normalizers_returning_empty_string_for_none(self): + def test_normalizers_handling_none(self): # Test that any normalizer can handle None as a valid value t = Task(self.tw) - normalizers = [getattr(t, normalizer_name) for normalizer_name in - filter(lambda x: x.startswith('normalize_'), dir(t))] - for normalizer in normalizers: - normalizer(None) + + for key in TASK_STANDARD_ATTRS: + t._normalize(key, None) def test_recurrent_task_generation(self): today = datetime.date.today() @@ -510,12 +745,40 @@ class TaskTest(TasklibTest): t.save() self.assertEqual(len(self.tw.tasks.pending()), 2) + def test_spawned_task_parent(self): + today = datetime.date.today() + t = Task(self.tw, description="brush teeth", + due=today, recur="daily") + t.save() + + spawned = self.tw.tasks.pending().get(due=today) + assert spawned['parent'] == t + + def test_modify_number_of_tasks_at_once(self): + for i in range(1, 100): + Task(self.tw, description="test task %d" % i, tags=['test']).save() + + self.tw.execute_command(['+test', 'mod', 'unified', 'description']) + + def test_return_all_from_executed_command(self): + Task(self.tw, description="test task", tags=['test']).save() + out, err, rc = self.tw.execute_command(['count'], return_all=True) + self.assertEqual(rc, 0) + + def test_return_all_from_failed_executed_command(self): + Task(self.tw, description="test task", tags=['test']).save() + out, err, rc = self.tw.execute_command(['countinvalid'], + return_all=True, allow_failure=False) + self.assertNotEqual(rc, 0) + + class TaskFromHookTest(TasklibTest): input_add_data = six.StringIO( '{"description":"Buy some milk",' '"entry":"20141118T050231Z",' '"status":"pending",' + '"start":"20141119T152233Z",' '"uuid":"a360fc44-315c-4366-b70c-ea7e7520b749"}') input_modify_data = six.StringIO(input_add_data.getvalue() + '\n' + @@ -530,12 +793,13 @@ class TaskFromHookTest(TasklibTest): '"description":"test task"}') def test_setting_up_from_add_hook_input(self): - t = Task.from_input(input_file=self.input_add_data) + t = Task.from_input(input_file=self.input_add_data, backend=self.tw) self.assertEqual(t['description'], "Buy some milk") self.assertEqual(t.pending, True) def test_setting_up_from_modified_hook_input(self): - t = Task.from_input(input_file=self.input_modify_data, modify=True) + t = Task.from_input(input_file=self.input_modify_data, modify=True, + backend=self.tw) self.assertEqual(t['description'], "Buy some milk finally") self.assertEqual(t.pending, False) self.assertEqual(t.completed, True) @@ -543,7 +807,7 @@ class TaskFromHookTest(TasklibTest): self.assertEqual(t._original_data['status'], "pending") self.assertEqual(t._original_data['description'], "Buy some milk") self.assertEqual(set(t._modified_fields), - set(['status', 'description'])) + set(['status', 'description', 'start'])) def test_export_data(self): t = Task(self.tw, description="test task", @@ -588,7 +852,7 @@ class TimezoneAwareDatetimeTest(TasklibTest): def test_serialize_naive_datetime(self): t = Task(self.tw, description="task1", due=self.localtime_naive) - self.assertEqual(json.loads(t.export_data())['due'], + self.assertEqual(json.loads(t.export_data())['due'], self.utctime_aware.strftime(DATE_FORMAT)) def test_timezone_naive_date_setitem(self): @@ -608,7 +872,7 @@ class TimezoneAwareDatetimeTest(TasklibTest): def test_serialize_naive_date(self): t = Task(self.tw, description="task1", due=self.localdate_naive) - self.assertEqual(json.loads(t.export_data())['due'], + self.assertEqual(json.loads(t.export_data())['due'], self.utctime_aware.strftime(DATE_FORMAT)) def test_timezone_aware_datetime_setitem(self): @@ -628,9 +892,87 @@ class TimezoneAwareDatetimeTest(TasklibTest): def test_serialize_aware_datetime(self): t = Task(self.tw, description="task1", due=self.localtime_aware) - self.assertEqual(json.loads(t.export_data())['due'], + self.assertEqual(json.loads(t.export_data())['due'], self.utctime_aware.strftime(DATE_FORMAT)) +class DatetimeStringTest(TasklibTest): + + def test_simple_now_conversion(self): + if self.tw.version < six.text_type('2.4.0'): + # Python2.6 does not support SkipTest. As a workaround + # mark the test as passed by exiting. + if getattr(unittest, 'SkipTest', None) is not None: + raise unittest.SkipTest() + else: + return + + t = Task(self.tw, description="test task", due="now") + now = local_zone.localize(datetime.datetime.now()) + + # Assert that both times are not more than 5 seconds apart + if sys.version_info < (2,7): + self.assertTrue(total_seconds_2_6(now - t['due']) < 5) + self.assertTrue(total_seconds_2_6(t['due'] - now) < 5) + else: + self.assertTrue((now - t['due']).total_seconds() < 5) + self.assertTrue((t['due'] - now).total_seconds() < 5) + + def test_simple_eoy_conversion(self): + if self.tw.version < six.text_type('2.4.0'): + # Python2.6 does not support SkipTest. As a workaround + # mark the test as passed by exiting. + if getattr(unittest, 'SkipTest', None) is not None: + raise unittest.SkipTest() + else: + return + + t = Task(self.tw, description="test task", due="eoy") + now = local_zone.localize(datetime.datetime.now()) + eoy = local_zone.localize(datetime.datetime( + year=now.year, + month=12, + day=31, + hour=23, + minute=59, + second=59 + )) + self.assertEqual(eoy, t['due']) + + def test_complex_eoy_conversion(self): + if self.tw.version < six.text_type('2.4.0'): + # Python2.6 does not support SkipTest. As a workaround + # mark the test as passed by exiting. + if getattr(unittest, 'SkipTest', None) is not None: + raise unittest.SkipTest() + else: + return + + t = Task(self.tw, description="test task", due="eoy - 4 months") + now = local_zone.localize(datetime.datetime.now()) + due_date = local_zone.localize(datetime.datetime( + year=now.year, + month=12, + day=31, + hour=23, + minute=59, + second=59 + )) - datetime.timedelta(0,4 * 30 * 86400) + self.assertEqual(due_date, t['due']) + + def test_filtering_with_string_datetime(self): + if self.tw.version < six.text_type('2.4.0'): + # Python2.6 does not support SkipTest. As a workaround + # mark the test as passed by exiting. + if getattr(unittest, 'SkipTest', None) is not None: + raise unittest.SkipTest() + else: + return + + t = Task(self.tw, description="test task", + due=datetime.datetime.now() - datetime.timedelta(0,2)) + t.save() + self.assertEqual(len(self.tw.tasks.filter(due__before="now")), 1) + class AnnotationTest(TasklibTest): def setUp(self): @@ -693,9 +1035,255 @@ class AnnotationTest(TasklibTest): class UnicodeTest(TasklibTest): def test_unicode_task(self): - Task(self.tw, description="†åßk").save() + Task(self.tw, description=six.u("†åßk")).save() self.tw.tasks.get() + def test_filter_by_unicode_task(self): + Task(self.tw, description=six.u("†åßk")).save() + tasks = self.tw.tasks.filter(description=six.u("†åßk")) + self.assertEqual(len(tasks), 1) + def test_non_unicode_task(self): Task(self.tw, description="test task").save() self.tw.tasks.get() + +class ReadOnlyDictViewTest(unittest.TestCase): + + def setUp(self): + self.sample = dict(l=[1,2,3], d={'k':'v'}) + self.original_sample = copy.deepcopy(self.sample) + self.view = ReadOnlyDictView(self.sample) + + def test_readonlydictview_getitem(self): + l = self.view['l'] + self.assertEqual(l, self.sample['l']) + + # Assert that modification changed only copied value + l.append(4) + self.assertNotEqual(l, self.sample['l']) + + # Assert that viewed dict is not changed + self.assertEqual(self.sample, self.original_sample) + + def test_readonlydictview_contains(self): + self.assertEqual('l' in self.view, 'l' in self.sample) + self.assertEqual('d' in self.view, 'd' in self.sample) + self.assertEqual('k' in self.view, 'k' in self.sample) + + # Assert that viewed dict is not changed + self.assertEqual(self.sample, self.original_sample) + + def test_readonlydictview_iter(self): + self.assertEqual(list(k for k in self.view), + list(k for k in self.sample)) + + # Assert the view is correct after modification + self.sample['new'] = 'value' + self.assertEqual(list(k for k in self.view), + list(k for k in self.sample)) + + def test_readonlydictview_len(self): + self.assertEqual(len(self.view), len(self.sample)) + + # Assert the view is correct after modification + self.sample['new'] = 'value' + self.assertEqual(len(self.view), len(self.sample)) + + def test_readonlydictview_get(self): + l = self.view.get('l') + self.assertEqual(l, self.sample.get('l')) + + # Assert that modification changed only copied value + l.append(4) + self.assertNotEqual(l, self.sample.get('l')) + + # Assert that viewed dict is not changed + self.assertEqual(self.sample, self.original_sample) + + def test_readonlydict_items(self): + view_items = self.view.items() + sample_items = list(self.sample.items()) + self.assertEqual(view_items, sample_items) + + view_items.append('newkey') + self.assertNotEqual(view_items, sample_items) + self.assertEqual(self.sample, self.original_sample) + + def test_readonlydict_values(self): + view_values = self.view.values() + sample_values = list(self.sample.values()) + self.assertEqual(view_values, sample_values) + + view_list_item = list(filter(lambda x: type(x) is list, + view_values))[0] + view_list_item.append(4) + self.assertNotEqual(view_values, sample_values) + self.assertEqual(self.sample, self.original_sample) + + +class LazyUUIDTaskTest(TasklibTest): + + def setUp(self): + super(LazyUUIDTaskTest, self).setUp() + + self.stored = Task(self.tw, description="this is test task") + self.stored.save() + + self.lazy = LazyUUIDTask(self.tw, self.stored['uuid']) + + def test_uuid_non_conversion(self): + assert self.stored['uuid'] == self.lazy['uuid'] + assert type(self.lazy) is LazyUUIDTask + + def test_lazy_explicit_conversion(self): + assert type(self.lazy) is LazyUUIDTask + self.lazy.replace() + assert type(self.lazy) is Task + + def test_conversion_key(self): + assert self.stored['description'] == self.lazy['description'] + assert type(self.lazy) is Task + + def test_conversion_attribute(self): + assert type(self.lazy) is LazyUUIDTask + assert self.lazy.completed is False + assert type(self.lazy) is Task + + def test_normal_to_lazy_equality(self): + assert self.stored == self.lazy + assert type(self.lazy) is LazyUUIDTask + + def test_lazy_to_lazy_equality(self): + lazy1 = LazyUUIDTask(self.tw, self.stored['uuid']) + lazy2 = LazyUUIDTask(self.tw, self.stored['uuid']) + + assert lazy1 == lazy2 + assert type(lazy1) is LazyUUIDTask + assert type(lazy2) is LazyUUIDTask + + def test_lazy_in_queryset(self): + tasks = self.tw.tasks.filter(uuid=self.stored['uuid']) + + assert self.lazy in tasks + assert type(self.lazy) is LazyUUIDTask + + def test_lazy_saved(self): + assert self.lazy.saved is True + + def test_lazy_modified(self): + assert self.lazy.modified is False + + def test_lazy_modified_fields(self): + assert self.lazy._modified_fields == set() + + +class LazyUUIDTaskSetTest(TasklibTest): + + def setUp(self): + super(LazyUUIDTaskSetTest, self).setUp() + + self.task1 = Task(self.tw, description="task 1") + self.task2 = Task(self.tw, description="task 2") + self.task3 = Task(self.tw, description="task 3") + + self.task1.save() + self.task2.save() + self.task3.save() + + self.uuids = ( + self.task1['uuid'], + self.task2['uuid'], + self.task3['uuid'] + ) + + self.lazy = LazyUUIDTaskSet(self.tw, self.uuids) + + def test_length(self): + assert len(self.lazy) == 3 + assert type(self.lazy) is LazyUUIDTaskSet + + def test_contains(self): + assert self.task1 in self.lazy + assert self.task2 in self.lazy + assert self.task3 in self.lazy + assert type(self.lazy) is LazyUUIDTaskSet + + def test_eq_lazy(self): + new_lazy = LazyUUIDTaskSet(self.tw, self.uuids) + assert self.lazy == new_lazy + assert not self.lazy != new_lazy + assert type(self.lazy) is LazyUUIDTaskSet + + def test_eq_real(self): + assert self.lazy == self.tw.tasks.all() + assert self.tw.tasks.all() == self.lazy + assert not self.lazy != self.tw.tasks.all() + + assert type(self.lazy) is LazyUUIDTaskSet + + def test_union(self): + taskset = set([self.task1]) + lazyset = LazyUUIDTaskSet( + self.tw, + (self.task2['uuid'], self.task3['uuid']) + ) + + assert taskset | lazyset == self.lazy + assert lazyset | taskset == self.lazy + assert taskset.union(lazyset) == self.lazy + assert lazyset.union(taskset) == self.lazy + + lazyset |= taskset + assert lazyset == self.lazy + + def test_difference(self): + taskset = set([self.task1, self.task2]) + lazyset = LazyUUIDTaskSet( + self.tw, + (self.task2['uuid'], self.task3['uuid']) + ) + + assert taskset - lazyset == set([self.task1]) + assert lazyset - taskset == set([self.task3]) + assert taskset.difference(lazyset) == set([self.task1]) + assert lazyset.difference(taskset) == set([self.task3]) + + lazyset -= taskset + assert lazyset == set([self.task3]) + + def test_symmetric_difference(self): + taskset = set([self.task1, self.task2]) + lazyset = LazyUUIDTaskSet( + self.tw, + (self.task2['uuid'], self.task3['uuid']) + ) + + assert taskset ^ lazyset == set([self.task1, self.task3]) + assert lazyset ^ taskset == set([self.task1, self.task3]) + assert taskset.symmetric_difference(lazyset) == set([self.task1, self.task3]) + assert lazyset.symmetric_difference(taskset) == set([self.task1, self.task3]) + + lazyset ^= taskset + assert lazyset == set([self.task1, self.task3]) + + def test_intersection(self): + taskset = set([self.task1, self.task2]) + lazyset = LazyUUIDTaskSet( + self.tw, + (self.task2['uuid'], self.task3['uuid']) + ) + + assert taskset & lazyset == set([self.task2]) + assert lazyset & taskset == set([self.task2]) + assert taskset.intersection(lazyset) == set([self.task2]) + assert lazyset.intersection(taskset) == set([self.task2]) + + lazyset &= taskset + assert lazyset == set([self.task2]) + + +class TaskWarriorBackendTest(TasklibTest): + + def test_config(self): + assert self.tw.config['nag'] == "You have more urgent tasks." + assert self.tw.config['debug'] == "no"