|  | @@ -2,12 +2,14 @@ import unittest2 as unittest
 | 
	
		
			
				|  |  |  from StringIO import StringIO
 | 
	
		
			
				|  |  |  from datetime import datetime, timedelta
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +from pyparsing import ParseException
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  from billiard.utils.functional import wraps
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  from celery import conf
 | 
	
		
			
				|  |  |  from celery import task
 | 
	
		
			
				|  |  |  from celery import messaging
 | 
	
		
			
				|  |  | -from celery.task.schedules import crontab
 | 
	
		
			
				|  |  | +from celery.task.schedules import crontab, crontab_parser
 | 
	
		
			
				|  |  |  from celery.utils import timeutils
 | 
	
		
			
				|  |  |  from celery.utils import gen_unique_id
 | 
	
		
			
				|  |  |  from celery.result import EagerResult
 | 
	
	
		
			
				|  | @@ -487,6 +489,10 @@ class EveryMinutePeriodic(task.PeriodicTask):
 | 
	
		
			
				|  |  |      run_every = crontab()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +class QuarterlyPeriodic(task.PeriodicTask):
 | 
	
		
			
				|  |  | +    run_every = crontab(minute="*/15")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  class HourlyPeriodic(task.PeriodicTask):
 | 
	
		
			
				|  |  |      run_every = crontab(minute=30)
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -517,7 +523,100 @@ def patch_crontab_nowfun(cls, retval):
 | 
	
		
			
				|  |  |      return create_patcher
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -class test_crontab(unittest.TestCase):
 | 
	
		
			
				|  |  | +class test_crontab_parser(unittest.TestCase):
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_parse_star(self):
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser(24).parse('*'), set(range(24)))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser(60).parse('*'), set(range(60)))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser(7).parse('*'), set(range(7)))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_parse_range(self):
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser(60).parse('1-10'), set(range(1,10+1)))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser(24).parse('0-20'), set(range(0,20+1)))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser().parse('2-10'), set(range(2,10+1)))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_parse_groups(self):
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser().parse('1,2,3,4'), set([1,2,3,4]))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser().parse('0,15,30,45'), set([0,15,30,45]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_parse_steps(self):
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser(8).parse('*/2'), set([0,2,4,6]))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser().parse('*/2'), set([ i*2 for i in xrange(30) ]))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser().parse('*/3'), set([ i*3 for i in xrange(20) ]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_parse_composite(self):
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser(8).parse('*/2'), set([0,2,4,6]))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser().parse('2-9/5'), set([5]))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser().parse('2-10/5'), set([5,10]))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser().parse('2-11/5,3'), set([3,5,10]))
 | 
	
		
			
				|  |  | +        self.assertEquals(crontab_parser().parse('2-4/3,*/5,0-21/4'), set([0,3,4,5,8,10,12,15,16,20,25,30,35,40,45,50,55]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_parse_errors_on_empty_string(self):
 | 
	
		
			
				|  |  | +        self.assertRaises(ParseException, crontab_parser(60).parse, '')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_parse_errors_on_empty_group(self):
 | 
	
		
			
				|  |  | +        self.assertRaises(ParseException, crontab_parser(60).parse, '1,,2')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_parse_errors_on_empty_steps(self):
 | 
	
		
			
				|  |  | +        self.assertRaises(ParseException, crontab_parser(60).parse, '*/')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_parse_errors_on_negative_number(self):
 | 
	
		
			
				|  |  | +        self.assertRaises(ParseException, crontab_parser(60).parse, '-20')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class test_crontab_is_due(unittest.TestCase):
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_default_crontab_spec(self):
 | 
	
		
			
				|  |  | +        c = crontab()
 | 
	
		
			
				|  |  | +        self.assertEquals(c.minute, set(range(60)))
 | 
	
		
			
				|  |  | +        self.assertEquals(c.hour, set(range(24)))
 | 
	
		
			
				|  |  | +        self.assertEquals(c.day_of_week, set(range(7)))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_simple_crontab_spec(self):
 | 
	
		
			
				|  |  | +        c = crontab(minute=30)
 | 
	
		
			
				|  |  | +        self.assertEquals(c.minute, set([30]))
 | 
	
		
			
				|  |  | +        self.assertEquals(c.hour, set(range(24)))
 | 
	
		
			
				|  |  | +        self.assertEquals(c.day_of_week, set(range(7)))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_crontab_spec_minute_formats(self):
 | 
	
		
			
				|  |  | +        c = crontab(minute=30)
 | 
	
		
			
				|  |  | +        self.assertEquals(c.minute, set([30]))
 | 
	
		
			
				|  |  | +        c = crontab(minute='30')
 | 
	
		
			
				|  |  | +        self.assertEquals(c.minute, set([30]))
 | 
	
		
			
				|  |  | +        c = crontab(minute=(30,40,50))
 | 
	
		
			
				|  |  | +        self.assertEquals(c.minute, set([30,40,50]))
 | 
	
		
			
				|  |  | +        c = crontab(minute=set([30,40,50]))
 | 
	
		
			
				|  |  | +        self.assertEquals(c.minute, set([30,40,50]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_crontab_spec_invalid_minute(self):
 | 
	
		
			
				|  |  | +        self.assertRaises(ValueError, crontab, minute=60)
 | 
	
		
			
				|  |  | +        self.assertRaises(ValueError, crontab, minute='0-100')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_crontab_spec_hour_formats(self):
 | 
	
		
			
				|  |  | +        c = crontab(hour=6)
 | 
	
		
			
				|  |  | +        self.assertEquals(c.hour, set([6]))
 | 
	
		
			
				|  |  | +        c = crontab(hour='5')
 | 
	
		
			
				|  |  | +        self.assertEquals(c.hour, set([5]))
 | 
	
		
			
				|  |  | +        c = crontab(hour=(4,8,12))
 | 
	
		
			
				|  |  | +        self.assertEquals(c.hour, set([4,8,12]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_crontab_spec_invalid_hour(self):
 | 
	
		
			
				|  |  | +        self.assertRaises(ValueError, crontab, hour=24)
 | 
	
		
			
				|  |  | +        self.assertRaises(ValueError, crontab, hour='0-30')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def test_crontab_spec_dow_formats(self):
 | 
	
		
			
				|  |  | +        c = crontab(day_of_week=5)
 | 
	
		
			
				|  |  | +        self.assertEquals(c.day_of_week, set([5]))
 | 
	
		
			
				|  |  | +        c = crontab(day_of_week='5')
 | 
	
		
			
				|  |  | +        self.assertEquals(c.day_of_week, set([5]))
 | 
	
		
			
				|  |  | +        c = crontab(day_of_week='fri')
 | 
	
		
			
				|  |  | +        self.assertEquals(c.day_of_week, set([5]))
 | 
	
		
			
				|  |  | +        c = crontab(day_of_week='tuesday,sunday,fri')
 | 
	
		
			
				|  |  | +        self.assertEquals(c.day_of_week, set([0,2,5]))
 | 
	
		
			
				|  |  | +        c = crontab(day_of_week='mon-fri')
 | 
	
		
			
				|  |  | +        self.assertEquals(c.day_of_week, set([1,2,3,4,5]))
 | 
	
		
			
				|  |  | +        c = crontab(day_of_week='*/2')
 | 
	
		
			
				|  |  | +        self.assertEquals(c.day_of_week, set([0,2,4,6]))
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def test_every_minute_execution_is_due(self):
 | 
	
		
			
				|  |  |          last_ran = datetime.now() - timedelta(seconds=61)
 | 
	
	
		
			
				|  | @@ -543,6 +642,30 @@ class test_crontab(unittest.TestCase):
 | 
	
		
			
				|  |  |          self.assertFalse(due)
 | 
	
		
			
				|  |  |          self.assertEquals(remaining, 1)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @patch_crontab_nowfun(QuarterlyPeriodic, datetime(2010, 5, 10, 10, 15))
 | 
	
		
			
				|  |  | +    def test_first_quarter_execution_is_due(self):
 | 
	
		
			
				|  |  | +        due, remaining = QuarterlyPeriodic().is_due(datetime(2010, 5, 10, 6, 30))
 | 
	
		
			
				|  |  | +        self.assertTrue(due)
 | 
	
		
			
				|  |  | +        self.assertEquals(remaining, 1)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @patch_crontab_nowfun(QuarterlyPeriodic, datetime(2010, 5, 10, 10, 30))
 | 
	
		
			
				|  |  | +    def test_second_quarter_execution_is_due(self):
 | 
	
		
			
				|  |  | +        due, remaining = QuarterlyPeriodic().is_due(datetime(2010, 5, 10, 6, 30))
 | 
	
		
			
				|  |  | +        self.assertTrue(due)
 | 
	
		
			
				|  |  | +        self.assertEquals(remaining, 1)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @patch_crontab_nowfun(QuarterlyPeriodic, datetime(2010, 5, 10, 10, 14))
 | 
	
		
			
				|  |  | +    def test_first_quarter_execution_is_not_due(self):
 | 
	
		
			
				|  |  | +        due, remaining = QuarterlyPeriodic().is_due(datetime(2010, 5, 10, 6, 30))
 | 
	
		
			
				|  |  | +        self.assertFalse(due)
 | 
	
		
			
				|  |  | +        self.assertEquals(remaining, 1)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @patch_crontab_nowfun(QuarterlyPeriodic, datetime(2010, 5, 10, 10, 29))
 | 
	
		
			
				|  |  | +    def test_second_quarter_execution_is_not_due(self):
 | 
	
		
			
				|  |  | +        due, remaining = QuarterlyPeriodic().is_due(datetime(2010, 5, 10, 6, 30))
 | 
	
		
			
				|  |  | +        self.assertFalse(due)
 | 
	
		
			
				|  |  | +        self.assertEquals(remaining, 1)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      @patch_crontab_nowfun(DailyPeriodic, datetime(2010, 5, 10, 7, 30))
 | 
	
		
			
				|  |  |      def test_daily_execution_is_due(self):
 | 
	
		
			
				|  |  |          due, remaining = DailyPeriodic().is_due(datetime(2010, 5, 9, 7, 30))
 |