1<?php
2
3namespace Cron\Tests;
4
5use Cron\CronExpression;
6use DateTime;
7use DateTimeZone;
8use InvalidArgumentException;
9use PHPUnit_Framework_TestCase;
10
11/**
12 * @author Michael Dowling <mtdowling@gmail.com>
13 */
14class CronExpressionTest extends PHPUnit_Framework_TestCase
15{
16    /**
17     * @covers Cron\CronExpression::factory
18     */
19    public function testFactoryRecognizesTemplates()
20    {
21        $this->assertEquals('0 0 1 1 *', CronExpression::factory('@annually')->getExpression());
22        $this->assertEquals('0 0 1 1 *', CronExpression::factory('@yearly')->getExpression());
23        $this->assertEquals('0 0 * * 0', CronExpression::factory('@weekly')->getExpression());
24    }
25
26    /**
27     * @covers Cron\CronExpression::__construct
28     * @covers Cron\CronExpression::getExpression
29     * @covers Cron\CronExpression::__toString
30     */
31    public function testParsesCronSchedule()
32    {
33        // '2010-09-10 12:00:00'
34        $cron = CronExpression::factory('1 2-4 * 4,5,6 */3');
35        $this->assertEquals('1', $cron->getExpression(CronExpression::MINUTE));
36        $this->assertEquals('2-4', $cron->getExpression(CronExpression::HOUR));
37        $this->assertEquals('*', $cron->getExpression(CronExpression::DAY));
38        $this->assertEquals('4,5,6', $cron->getExpression(CronExpression::MONTH));
39        $this->assertEquals('*/3', $cron->getExpression(CronExpression::WEEKDAY));
40        $this->assertEquals('1 2-4 * 4,5,6 */3', $cron->getExpression());
41        $this->assertEquals('1 2-4 * 4,5,6 */3', (string) $cron);
42        $this->assertNull($cron->getExpression('foo'));
43
44        try {
45            $cron = CronExpression::factory('A 1 2 3 4');
46            $this->fail('Validation exception not thrown');
47        } catch (InvalidArgumentException $e) {
48        }
49    }
50
51    /**
52     * @covers Cron\CronExpression::__construct
53     * @covers Cron\CronExpression::getExpression
54     * @dataProvider scheduleWithDifferentSeparatorsProvider
55     */
56    public function testParsesCronScheduleWithAnySpaceCharsAsSeparators($schedule, array $expected)
57    {
58        $cron = CronExpression::factory($schedule);
59        $this->assertEquals($expected[0], $cron->getExpression(CronExpression::MINUTE));
60        $this->assertEquals($expected[1], $cron->getExpression(CronExpression::HOUR));
61        $this->assertEquals($expected[2], $cron->getExpression(CronExpression::DAY));
62        $this->assertEquals($expected[3], $cron->getExpression(CronExpression::MONTH));
63        $this->assertEquals($expected[4], $cron->getExpression(CronExpression::WEEKDAY));
64        $this->assertEquals($expected[5], $cron->getExpression(CronExpression::YEAR));
65    }
66
67    /**
68     * Data provider for testParsesCronScheduleWithAnySpaceCharsAsSeparators
69     *
70     * @return array
71     */
72    public static function scheduleWithDifferentSeparatorsProvider()
73    {
74        return array(
75            array("*\t*\t*\t*\t*\t*", array('*', '*', '*', '*', '*', '*')),
76            array("*  *  *  *  *  *", array('*', '*', '*', '*', '*', '*')),
77            array("* \t * \t * \t * \t * \t *", array('*', '*', '*', '*', '*', '*')),
78            array("*\t \t*\t \t*\t \t*\t \t*\t \t*", array('*', '*', '*', '*', '*', '*')),
79        );
80    }
81
82    /**
83     * @covers Cron\CronExpression::__construct
84     * @covers Cron\CronExpression::setExpression
85     * @covers Cron\CronExpression::setPart
86     * @expectedException InvalidArgumentException
87     */
88    public function testInvalidCronsWillFail()
89    {
90        // Only four values
91        $cron = CronExpression::factory('* * * 1');
92    }
93
94    /**
95     * @covers Cron\CronExpression::setPart
96     * @expectedException InvalidArgumentException
97     */
98    public function testInvalidPartsWillFail()
99    {
100        // Only four values
101        $cron = CronExpression::factory('* * * * *');
102        $cron->setPart(1, 'abc');
103    }
104
105    /**
106     * Data provider for cron schedule
107     *
108     * @return array
109     */
110    public function scheduleProvider()
111    {
112        return array(
113            array('*/2 */2 * * *', '2015-08-10 21:47:27', '2015-08-10 22:00:00', false),
114            array('* * * * *', '2015-08-10 21:50:37', '2015-08-10 21:50:00', true),
115            array('* 20,21,22 * * *', '2015-08-10 21:50:00', '2015-08-10 21:50:00', true),
116            // Handles CSV values
117            array('* 20,22 * * *', '2015-08-10 21:50:00', '2015-08-10 22:00:00', false),
118            // CSV values can be complex
119            array('* 5,21-22 * * *', '2015-08-10 21:50:00', '2015-08-10 21:50:00', true),
120            array('7-9 * */9 * *', '2015-08-10 22:02:33', '2015-08-18 00:07:00', false),
121            // 15th minute, of the second hour, every 15 days, in January, every Friday
122            array('1 * * * 7', '2015-08-10 21:47:27', '2015-08-16 00:01:00', false),
123            // Test with exact times
124            array('47 21 * * *', strtotime('2015-08-10 21:47:30'), '2015-08-10 21:47:00', true),
125            // Test Day of the week (issue #1)
126            // According cron implementation, 0|7 = sunday, 1 => monday, etc
127            array('* * * * 0', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
128            array('* * * * 7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
129            array('* * * * 1', strtotime('2011-06-15 23:09:00'), '2011-06-20 00:00:00', false),
130            // Should return the sunday date as 7 equals 0
131            array('0 0 * * MON,SUN', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
132            array('0 0 * * 1,7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
133            array('0 0 * * 0-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
134            array('0 0 * * 7-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
135            array('0 0 * * 4-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
136            array('0 0 * * 7-3', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
137            array('0 0 * * 3-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
138            array('0 0 * * 3-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false),
139            // Test lists of values and ranges (Abhoryo)
140            array('0 0 * * 2-7', strtotime('2011-06-20 23:09:00'), '2011-06-21 00:00:00', false),
141            array('0 0 * * 0,2-6', strtotime('2011-06-20 23:09:00'), '2011-06-21 00:00:00', false),
142            array('0 0 * * 2-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false),
143            array('0 0 * * 4-7', strtotime('2011-07-19 00:00:00'), '2011-07-21 00:00:00', false),
144            // Test increments of ranges
145            array('0-12/4 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true),
146            array('4-59/2 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true),
147            array('4-59/2 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:06:00', true),
148            array('4-59/3 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:07:00', false),
149            //array('0 0 * * 0,2-6', strtotime('2011-06-20 23:09:00'), '2011-06-21 00:00:00', false),
150            // Test Day of the Week and the Day of the Month (issue #1)
151            array('0 0 1 1 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false),
152            array('0 0 1 JAN 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false),
153            array('0 0 1 * 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false),
154            array('0 0 L * *', strtotime('2011-07-15 00:00:00'), '2011-07-31 00:00:00', false),
155            // Test the W day of the week modifier for day of the month field
156            array('0 0 2W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true),
157            array('0 0 1W * *', strtotime('2011-05-01 00:00:00'), '2011-05-02 00:00:00', false),
158            array('0 0 1W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true),
159            array('0 0 3W * *', strtotime('2011-07-01 00:00:00'), '2011-07-04 00:00:00', false),
160            array('0 0 16W * *', strtotime('2011-07-01 00:00:00'), '2011-07-15 00:00:00', false),
161            array('0 0 28W * *', strtotime('2011-07-01 00:00:00'), '2011-07-28 00:00:00', false),
162            array('0 0 30W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false),
163            array('0 0 31W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false),
164            // Test the year field
165            array('* * * * * 2012', strtotime('2011-05-01 00:00:00'), '2012-01-01 00:00:00', false),
166            // Test the last weekday of a month
167            array('* * * * 5L', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false),
168            array('* * * * 6L', strtotime('2011-07-01 00:00:00'), '2011-07-30 00:00:00', false),
169            array('* * * * 7L', strtotime('2011-07-01 00:00:00'), '2011-07-31 00:00:00', false),
170            array('* * * * 1L', strtotime('2011-07-24 00:00:00'), '2011-07-25 00:00:00', false),
171            array('* * * * TUEL', strtotime('2011-07-24 00:00:00'), '2011-07-26 00:00:00', false),
172            array('* * * 1 5L', strtotime('2011-12-25 00:00:00'), '2012-01-27 00:00:00', false),
173            // Test the hash symbol for the nth weekday of a given month
174            array('* * * * 5#2', strtotime('2011-07-01 00:00:00'), '2011-07-08 00:00:00', false),
175            array('* * * * 5#1', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true),
176            array('* * * * 3#4', strtotime('2011-07-01 00:00:00'), '2011-07-27 00:00:00', false),
177        );
178    }
179
180    /**
181     * @covers Cron\CronExpression::isDue
182     * @covers Cron\CronExpression::getNextRunDate
183     * @covers Cron\DayOfMonthField
184     * @covers Cron\DayOfWeekField
185     * @covers Cron\MinutesField
186     * @covers Cron\HoursField
187     * @covers Cron\MonthField
188     * @covers Cron\YearField
189     * @covers Cron\CronExpression::getRunDate
190     * @dataProvider scheduleProvider
191     */
192    public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $isDue)
193    {
194        $relativeTimeString = is_int($relativeTime) ? date('Y-m-d H:i:s', $relativeTime) : $relativeTime;
195
196        // Test next run date
197        $cron = CronExpression::factory($schedule);
198        if (is_string($relativeTime)) {
199            $relativeTime = new DateTime($relativeTime);
200        } elseif (is_int($relativeTime)) {
201            $relativeTime = date('Y-m-d H:i:s', $relativeTime);
202        }
203        $this->assertEquals($isDue, $cron->isDue($relativeTime));
204        $next = $cron->getNextRunDate($relativeTime, 0, true);
205        $this->assertEquals(new DateTime($nextRun), $next);
206    }
207
208    /**
209     * @covers Cron\CronExpression::isDue
210     */
211    public function testIsDueHandlesDifferentDates()
212    {
213        $cron = CronExpression::factory('* * * * *');
214        $this->assertTrue($cron->isDue());
215        $this->assertTrue($cron->isDue('now'));
216        $this->assertTrue($cron->isDue(new DateTime('now')));
217        $this->assertTrue($cron->isDue(date('Y-m-d H:i')));
218    }
219
220    /**
221     * @covers Cron\CronExpression::isDue
222     */
223    public function testIsDueHandlesDifferentTimezones()
224    {
225        $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00
226        $date = '2014-01-01 15:00'; //Wednesday
227        $utc = new DateTimeZone('UTC');
228        $amsterdam =  new DateTimeZone('Europe/Amsterdam');
229        $tokyo = new DateTimeZone('Asia/Tokyo');
230
231        date_default_timezone_set('UTC');
232        $this->assertTrue($cron->isDue(new DateTime($date, $utc)));
233        $this->assertFalse($cron->isDue(new DateTime($date, $amsterdam)));
234        $this->assertFalse($cron->isDue(new DateTime($date, $tokyo)));
235
236        date_default_timezone_set('Europe/Amsterdam');
237        $this->assertFalse($cron->isDue(new DateTime($date, $utc)));
238        $this->assertTrue($cron->isDue(new DateTime($date, $amsterdam)));
239        $this->assertFalse($cron->isDue(new DateTime($date, $tokyo)));
240
241        date_default_timezone_set('Asia/Tokyo');
242        $this->assertFalse($cron->isDue(new DateTime($date, $utc)));
243        $this->assertFalse($cron->isDue(new DateTime($date, $amsterdam)));
244        $this->assertTrue($cron->isDue(new DateTime($date, $tokyo)));
245    }
246
247    /**
248     * @covers Cron\CronExpression::getPreviousRunDate
249     */
250    public function testCanGetPreviousRunDates()
251    {
252        $cron = CronExpression::factory('* * * * *');
253        $next = $cron->getNextRunDate('now');
254        $two = $cron->getNextRunDate('now', 1);
255        $this->assertEquals($next, $cron->getPreviousRunDate($two));
256
257        $cron = CronExpression::factory('* */2 * * *');
258        $next = $cron->getNextRunDate('now');
259        $two = $cron->getNextRunDate('now', 1);
260        $this->assertEquals($next, $cron->getPreviousRunDate($two));
261
262        $cron = CronExpression::factory('* * * */2 *');
263        $next = $cron->getNextRunDate('now');
264        $two = $cron->getNextRunDate('now', 1);
265        $this->assertEquals($next, $cron->getPreviousRunDate($two));
266    }
267
268    /**
269     * @covers Cron\CronExpression::getMultipleRunDates
270     */
271    public function testProvidesMultipleRunDates()
272    {
273        $cron = CronExpression::factory('*/2 * * * *');
274        $this->assertEquals(array(
275            new DateTime('2008-11-09 00:00:00'),
276            new DateTime('2008-11-09 00:02:00'),
277            new DateTime('2008-11-09 00:04:00'),
278            new DateTime('2008-11-09 00:06:00')
279        ), $cron->getMultipleRunDates(4, '2008-11-09 00:00:00', false, true));
280    }
281
282    /**
283     * @covers Cron\CronExpression::getMultipleRunDates
284     * @covers Cron\CronExpression::setMaxIterationCount
285     */
286    public function testProvidesMultipleRunDatesForTheFarFuture() {
287        // Fails with the default 1000 iteration limit
288        $cron = CronExpression::factory('0 0 12 1 * */2');
289        $cron->setMaxIterationCount(2000);
290        $this->assertEquals(array(
291            new DateTime('2016-01-12 00:00:00'),
292            new DateTime('2018-01-12 00:00:00'),
293            new DateTime('2020-01-12 00:00:00'),
294            new DateTime('2022-01-12 00:00:00'),
295            new DateTime('2024-01-12 00:00:00'),
296            new DateTime('2026-01-12 00:00:00'),
297            new DateTime('2028-01-12 00:00:00'),
298            new DateTime('2030-01-12 00:00:00'),
299            new DateTime('2032-01-12 00:00:00'),
300        ), $cron->getMultipleRunDates(9, '2015-04-28 00:00:00', false, true));
301    }
302
303    /**
304     * @covers Cron\CronExpression
305     */
306    public function testCanIterateOverNextRuns()
307    {
308        $cron = CronExpression::factory('@weekly');
309        $nextRun = $cron->getNextRunDate("2008-11-09 08:00:00");
310        $this->assertEquals($nextRun, new DateTime("2008-11-16 00:00:00"));
311
312        // true is cast to 1
313        $nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", true, true);
314        $this->assertEquals($nextRun, new DateTime("2008-11-16 00:00:00"));
315
316        // You can iterate over them
317        $nextRun = $cron->getNextRunDate($cron->getNextRunDate("2008-11-09 00:00:00", 1, true), 1, true);
318        $this->assertEquals($nextRun, new DateTime("2008-11-23 00:00:00"));
319
320        // You can skip more than one
321        $nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", 2, true);
322        $this->assertEquals($nextRun, new DateTime("2008-11-23 00:00:00"));
323        $nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", 3, true);
324        $this->assertEquals($nextRun, new DateTime("2008-11-30 00:00:00"));
325    }
326
327    /**
328     * @covers Cron\CronExpression::getRunDate
329     */
330    public function testSkipsCurrentDateByDefault()
331    {
332        $cron = CronExpression::factory('* * * * *');
333        $current = new DateTime('now');
334        $next = $cron->getNextRunDate($current);
335        $nextPrev = $cron->getPreviousRunDate($next);
336        $this->assertEquals($current->format('Y-m-d H:i:00'), $nextPrev->format('Y-m-d H:i:s'));
337    }
338
339    /**
340     * @covers Cron\CronExpression::getRunDate
341     * @ticket 7
342     */
343    public function testStripsForSeconds()
344    {
345        $cron = CronExpression::factory('* * * * *');
346        $current = new DateTime('2011-09-27 10:10:54');
347        $this->assertEquals('2011-09-27 10:11:00', $cron->getNextRunDate($current)->format('Y-m-d H:i:s'));
348    }
349
350    /**
351     * @covers Cron\CronExpression::getRunDate
352     */
353    public function testFixesPhpBugInDateIntervalMonth()
354    {
355        $cron = CronExpression::factory('0 0 27 JAN *');
356        $this->assertEquals('2011-01-27 00:00:00', $cron->getPreviousRunDate('2011-08-22 00:00:00')->format('Y-m-d H:i:s'));
357    }
358
359    public function testIssue29()
360    {
361        $cron = CronExpression::factory('@weekly');
362        $this->assertEquals(
363            '2013-03-10 00:00:00',
364            $cron->getPreviousRunDate('2013-03-17 00:00:00')->format('Y-m-d H:i:s')
365        );
366    }
367
368    /**
369     * @see https://github.com/mtdowling/cron-expression/issues/20
370     */
371    public function testIssue20() {
372        $e = CronExpression::factory('* * * * MON#1');
373        $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00')));
374        $this->assertFalse($e->isDue(new DateTime('2014-04-14 00:00:00')));
375        $this->assertFalse($e->isDue(new DateTime('2014-04-21 00:00:00')));
376
377        $e = CronExpression::factory('* * * * SAT#2');
378        $this->assertFalse($e->isDue(new DateTime('2014-04-05 00:00:00')));
379        $this->assertTrue($e->isDue(new DateTime('2014-04-12 00:00:00')));
380        $this->assertFalse($e->isDue(new DateTime('2014-04-19 00:00:00')));
381
382        $e = CronExpression::factory('* * * * SUN#3');
383        $this->assertFalse($e->isDue(new DateTime('2014-04-13 00:00:00')));
384        $this->assertTrue($e->isDue(new DateTime('2014-04-20 00:00:00')));
385        $this->assertFalse($e->isDue(new DateTime('2014-04-27 00:00:00')));
386    }
387
388    /**
389     * @covers Cron\CronExpression::getRunDate
390     */
391    public function testKeepOriginalTime()
392    {
393        $now = new \DateTime;
394        $strNow = $now->format(DateTime::ISO8601);
395        $cron = CronExpression::factory('0 0 * * *');
396        $cron->getPreviousRunDate($now);
397        $this->assertEquals($strNow, $now->format(DateTime::ISO8601));
398    }
399
400    /**
401     * @covers Cron\CronExpression::__construct
402     * @covers Cron\CronExpression::factory
403     * @covers Cron\CronExpression::isValidExpression
404     * @covers Cron\CronExpression::setExpression
405     * @covers Cron\CronExpression::setPart
406     */
407    public function testValidationWorks()
408    {
409        // Invalid. Only four values
410        $this->assertFalse(CronExpression::isValidExpression('* * * 1'));
411        // Valid
412        $this->assertTrue(CronExpression::isValidExpression('* * * * 1'));
413    }
414}
415