Skip to content

Commit

Permalink
Further clean up of Args and Kwargs implementation + full test coverage
Browse files Browse the repository at this point in the history
Key improvements include testing the pass-through of parameters to the created Job object.
  • Loading branch information
tom-price committed Jul 27, 2018
1 parent 923ed26 commit 5d87779
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 34 deletions.
37 changes: 25 additions & 12 deletions scheduler/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,23 @@ class BaseJobArg(models.Model):
('int_val', _('int')),
('datetime_val', _('Datetime')),
)
str_val = models.CharField(_('String Value'), blank=True, max_length=255)
int_val = models.IntegerField(_('Int Value'), blank=True, null=True)
datetime_val = models.DateTimeField(_('Datetime Value'), blank=True, null=True)

arg_name = models.CharField(
_('Argument Type'), max_length=12, choices=ARG_NAME, default=ARG_NAME.str_val
)
str_val = models.CharField(_('String Value'), blank=True, max_length=255)
int_val = models.IntegerField(_('Int Value'), blank=True, null=True)
datetime_val = models.DateTimeField(_('Datetime Value'), blank=True, null=True)

content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey()

def __repr__(self):
return repr(getattr(self, self.arg_name))

def __str__(self):
return str(getattr(self, self.arg_name))

def clean(self):
self.clean_one_value()

Expand All @@ -57,6 +62,9 @@ def clean_one_value(self):
_('There are multiple arg types with values'), code='invalid')
})

def value(self):
return getattr(self, self.arg_name)

class Meta:
abstract = True

Expand All @@ -68,14 +76,17 @@ class JobArg(BaseJobArg):
class JobKwarg(BaseJobArg):
key = models.CharField(max_length=255)

def value(self):
return self.key, super(JobKwarg, self).value()


@python_2_unicode_compatible
class BaseJob(TimeStampedModel):

name = models.CharField(_('name'), max_length=128, unique=True)
callable = models.CharField(_('callable'), max_length=2048)
callable_args = GenericRelation(JobArg)
callable_kwargs = GenericRelation(JobKwarg)
callable_args = GenericRelation(JobArg, related_query_name='args')
callable_kwargs = GenericRelation(JobKwarg, related_query_name='kwargs')
enabled = models.BooleanField(_('enabled'), default=True)
queue = models.CharField(_('queue'), max_length=16)
job_id = models.CharField(
Expand Down Expand Up @@ -160,12 +171,12 @@ def unschedule(self):
return True

def parse_args(self):
args = self.callable_args.values().order_by('id')
return [arg[arg['arg_name']] for arg in args]
args = self.callable_args.all().order_by('id')
return [arg.value() for arg in args]

def parse_kwargs(self):
kwargs = self.callable_kwargs.values().order_by('id')
return {kwarg['key']: kwarg[kwarg['arg_name']] for kwarg in kwargs}
kwargs = self.callable_kwargs.all().order_by('id')
return dict([kwarg.value() for kwarg in kwargs])

def function_string(self):
func = self.callable + "(\u200b{})" # zero-width space allows textwrap
Expand Down Expand Up @@ -198,13 +209,15 @@ class ScheduledJob(ScheduledTimeMixin, BaseJob):
def schedule(self):
if self.is_schedulable() is False:
return False
kwargs = {}
kwargs = self.parse_kwargs()
if self.timeout:
kwargs['timeout'] = self.timeout
if self.result_ttl is not None:
kwargs['job_result_ttl'] = self.result_ttl
job = self.scheduler().enqueue_at(
self.schedule_time_utc(), self.callable_func(),
self.schedule_time_utc(),
self.callable_func(),
*self.parse_args(),
**kwargs
)
self.job_id = job.id
Expand Down
200 changes: 178 additions & 22 deletions scheduler/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime, timedelta

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
Expand All @@ -12,37 +13,36 @@
import pytz
import django_rq
from django_rq import job
from scheduler.models import BaseJob, CronJob, RepeatableJob, ScheduledJob
from scheduler.models import BaseJob, CronJob, RepeatableJob, ScheduledJob,\
BaseJobArg, JobArg, JobKwarg
import django_rq.queues


class BaseJobFactory(factory.DjangoModelFactory):
class Meta:
django_get_or_create = ('name',)

name = factory.Sequence(lambda n: 'Scheduled Job %d' % n)
queue = list(settings.RQ_QUEUES.keys())[0]
callable = 'scheduler.tests.test_job'
enabled = True
timeout = None
# job_id = None


class ScheduledJobFactory(BaseJobFactory):
class Meta:
model = ScheduledJob
django_get_or_create = ('name',)
abstract = True


class ScheduledJobFactory(BaseJobFactory):
result_ttl = None

@factory.lazy_attribute
def scheduled_time(self):
return timezone.now() + timedelta(days=1)


class RepeatableJobFactory(BaseJobFactory):
class Meta:
model = RepeatableJob
model = ScheduledJob


class RepeatableJobFactory(BaseJobFactory):
result_ttl = None
interval = 1
interval_unit = 'hours'
Expand All @@ -52,13 +52,43 @@ class Meta:
def scheduled_time(self):
return timezone.now() + timedelta(minutes=1)

class Meta:
model = RepeatableJob


class CronJobFactory(BaseJobFactory):
cron_string = "0 0 * * *"
repeat = None

class Meta:
model = CronJob

cron_string = "0 0 * * *"
repeat = None

class BaseJobArgFactory(factory.DjangoModelFactory):
arg_name = 'str_val'
str_val = ''
int_val = None
datetime_val = None
object_id = factory.SelfAttribute('content_object.id')
content_type = factory.LazyAttribute(
lambda o: ContentType.objects.get_for_model(o.content_object))
content_object = factory.SubFactory(ScheduledJobFactory)

class Meta:
exclude = ['content_object']
abstract = True


class JobArgFactory(BaseJobArgFactory):
class Meta:
model = JobArg


class JobKwargFactory(BaseJobArgFactory):
key = factory.Sequence(lambda n: 'key%d' % n)

class Meta:
model = JobKwarg


@job
Expand All @@ -68,25 +98,80 @@ def test_job():

@job
def test_args_kwargs(*args, **kwargs):
func = "test_args_kwargs({0}, {1})"
func = "test_args_kwargs({})"
args_list = [repr(arg) for arg in args]
arg_string = ', '.join(args_list)
kwarg_string = ', '.join([k + '=' + repr(kwargs[k]) for k in kwargs])
return func.format(arg_string, kwarg_string)
kwargs_list = [k + '=' + repr(v) for (k, v) in kwargs.items()]
return func.format(', '.join(args_list + kwargs_list))


test_non_callable = 'I am a teapot'


class BaseTestCases:

class TestBaseJobArg(TestCase):
JobArgClass = BaseJobArg
JobArgClassFactory = BaseJobArgFactory

def test_clean_one_value_empty(self):
arg = self.JobArgClassFactory()
with self.assertRaises(ValidationError):
arg.clean_one_value()

def test_clean_one_value_invalid_str_int(self):
arg = self.JobArgClassFactory(str_val='not blank', int_val=1, datetime_val=None)
with self.assertRaises(ValidationError):
arg.clean_one_value()

def test_clean_one_value_invalid_str_datetime(self):
arg = self.JobArgClassFactory(str_val='not blank', int_val=None, datetime_val=timezone.now())
with self.assertRaises(ValidationError):
arg.clean_one_value()

def test_clean_one_value_invalid_int_datetime(self):
arg = self.JobArgClassFactory(str_val= '', int_val=1, datetime_val=timezone.now())
with self.assertRaises(ValidationError):
arg.clean_one_value()

def test_clean_invalid(self):
arg = self.JobArgClassFactory(str_val='str', int_val=1, datetime_val=timezone.now())
with self.assertRaises(ValidationError):
arg.clean()

def test_clean(self):
arg = self.JobArgClassFactory(str_val='something')
self.assertIsNone(arg.clean())

def test__str__str_val(self):
arg = self.JobArgClassFactory(arg_name='str_val', str_val='something')
self.assertEqual('something', str(arg))

def test__str__int_val(self):
arg = self.JobArgClassFactory(arg_name='int_val', int_val=1)
self.assertEqual('1', str(arg))

def test__str__datetime_val(self):
time = datetime(2018, 1, 1)
arg = self.JobArgClassFactory(arg_name='datetime_val', datetime_val=time)
self.assertEqual('2018-01-01 00:00:00', str(arg))

def test__repr__str_val(self):
arg = self.JobArgClassFactory(arg_name='str_val', str_val='something')
self.assertEqual("'something'", repr(arg))

def test__repr__int_val(self):
arg = self.JobArgClassFactory(arg_name='int_val', int_val=1)
self.assertEqual('1', repr(arg))

def test__repr__datetime_val(self):
time = datetime(2018, 1, 1, 0, 0)
arg = self.JobArgClassFactory(arg_name='datetime_val', datetime_val=time)
self.assertEqual('datetime.datetime(2018, 1, 1, 0, 0)', repr(arg))

class TestBaseJob(TestCase):
def setUp(self):
django_rq.queues.get_redis_connection = lambda _, strict: FakeStrictRedis()
# Turn on synchronous mode to execute jobs immediately instead of passing them off to the workers
for config in settings.RQ_QUEUES.values():
config["ASYNC"] = False

# Never runs as BaseJob but helps IDE auto-completes
JobClass = BaseJob
JobClassFactory = BaseJobFactory

Expand Down Expand Up @@ -227,10 +312,63 @@ def test_timeout_passthroug(self):
entry = next(i for i in scheduler.get_jobs() if i.id == job.job_id)
self.assertEqual(entry.timeout, 500)

def test_parse_args(self):
job = self.JobClassFactory()
date = timezone.now()
JobArgFactory(str_val='one', content_object=job)
JobArgFactory(arg_name='int_val', int_val=2, content_object=job)
JobArgFactory(arg_name='datetime_val', datetime_val=date, content_object=job)
self.assertEqual(job.parse_args(), ['one', 2, date])

def test_parse_kwargs(self):
job = self.JobClassFactory()
date = timezone.now()
JobKwargFactory(key='key1', arg_name='str_val', str_val='one', content_object=job)
JobKwargFactory(key='key2', arg_name='int_val', int_val=2, content_object=job)
JobKwargFactory(key='key3', arg_name='datetime_val', datetime_val=date, content_object=job)
self.assertEqual(job.parse_kwargs(), dict(key1='one', key2=2, key3=date))

def test_function_string(self):
job = self.JobClassFactory()
date = datetime(2018, 1, 1)
JobArgFactory(arg_name='str_val', str_val='one', content_object=job)
JobArgFactory(arg_name='int_val', int_val=1, content_object=job)
JobArgFactory(arg_name='datetime_val', datetime_val=date, content_object=job)
JobKwargFactory(key='key1', arg_name='str_val', str_val='one', content_object=job)
JobKwargFactory(key='key2', arg_name='int_val', int_val=2, content_object=job)
JobKwargFactory(key='key3', arg_name='datetime_val', datetime_val=date, content_object=job)
self.assertEqual(job.function_string(),
("scheduler.tests.test_job(\u200b'one', 1, datetime.datetime(2018, 1, 1, 0, 0), " +
"key1='one', key2=2, key3=datetime.datetime(2018, 1, 1, 0, 0))"))

def test_callable_result(self):
job = self.JobClassFactory()
scheduler = django_rq.get_scheduler(job.queue)
entry = next(i for i in scheduler.get_jobs() if i.id == job.job_id)
self.assertEqual(entry.perform(), 2)

def test_callable_empty_args_and_kwagrgs(self):
job = self.JobClassFactory(callable='scheduler.tests.test_args_kwargs')
scheduler = django_rq.get_scheduler(job.queue)
entry = next(i for i in scheduler.get_jobs() if i.id == job.job_id)
self.assertEqual(entry.perform(), 'test_args_kwargs()')

def test_callable_args_and_kwagrgs(self):
job = self.JobClassFactory(callable='scheduler.tests.test_args_kwargs')
date = datetime(2018, 1, 1)
JobArgFactory(arg_name='str_val', str_val='one', content_object=job)
JobKwargFactory(key='key1', arg_name='int_val', int_val=2, content_object=job)
JobKwargFactory(key='key2', arg_name='datetime_val', datetime_val=date, content_object=job)
job.save()
scheduler = django_rq.get_scheduler(job.queue)
entry = next(i for i in scheduler.get_jobs() if i.id == job.job_id)
self.assertEqual(entry.perform(),
"test_args_kwargs('one', key1=2, key2=datetime.datetime(2018, 1, 1, 0, 0))")

class TestSchedulableJob(TestBaseJob):
# Currently ScheduledJob and RepeatableJob
JobClass = ScheduledJob
JobClassFactory = ScheduledJobFactory
JobClass = BaseJob
JobClassFactory = BaseJobFactory

def test_schedule_time_utc(self):
job = self.JobClass()
Expand All @@ -248,6 +386,24 @@ def test_result_ttl_passthroug(self):
self.assertEqual(entry.result_ttl, 500)


class TestJobArg(BaseTestCases.TestBaseJobArg):
JobArgClass = JobArg
JobArgClassFactory = JobArgFactory

def test_value(self):
arg = self.JobArgClassFactory(arg_name='str_val', str_val='something')
self.assertEqual(arg.value(), 'something')


class TestJobKwarg(BaseTestCases.TestBaseJobArg):
JobArgClass = JobKwarg
JobArgClassFactory = JobKwargFactory

def test_value(self):
kwarg = self.JobArgClassFactory(key='key', arg_name='str_val', str_val='value')
self.assertEqual(kwarg.value(), ('key', 'value'))


class TestScheduledJob(BaseTestCases.TestSchedulableJob):
JobClass = ScheduledJob
JobClassFactory = ScheduledJobFactory
Expand Down

0 comments on commit 5d87779

Please sign in to comment.