Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core\task;
18
 
19
defined('MOODLE_INTERNAL') || die();
20
require_once(__DIR__ . '/../fixtures/task_fixtures.php');
21
 
22
/**
23
 * Test class for scheduled task.
24
 *
25
 * @package core
26
 * @category test
27
 * @copyright 2013 Damyon Wiese
28
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29
 * @coversDefaultClass \core\task\scheduled_task
30
 */
31
class scheduled_task_test extends \advanced_testcase {
32
 
33
    /**
34
     * Data provider for {@see test_eval_cron_field}
35
     *
36
     * @return array
37
     */
38
    public static function eval_cron_provider(): array {
39
        return [
40
            // At every 3rd <unit>.
41
            ['*/3', 0, 29, [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]],
42
            // At <unit> 1 and every 2nd <unit>.
43
            ['1,*/2', 0, 29, [0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]],
44
            // At every <unit> from 1 through 10 and every <unit> from 5 through 15.
45
            ['1-10,5-15', 0, 29, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]],
46
            // At every <unit> from 1 through 10 and every 2nd <unit> from 5 through 15.
47
            ['1-10,5-15/2', 0, 29, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15]],
48
            // At every <unit> from 1 through 10 and every 2nd <unit> from 5 through 29.
49
            ['1-10,5/2', 0, 29, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]],
50
            // At <unit> 1, 2, 3.
51
            ['1,2,3,1,2,3', 0, 29, [1, 2, 3]],
52
            // Invalid.
53
            ['-1,10,80', 0, 29, []],
54
            // Invalid.
55
            ['-1', 0, 29, []],
56
        ];
57
    }
58
 
59
    /**
60
     * Test the cron scheduling method
61
     *
62
     * @param string $field
63
     * @param int $min
64
     * @param int $max
65
     * @param int[] $expected
66
     *
67
     * @dataProvider eval_cron_provider
68
     *
69
     * @covers ::eval_cron_field
70
     */
71
    public function test_eval_cron_field(string $field, int $min, int $max, array $expected): void {
72
        $testclass = new scheduled_test_task();
73
 
74
        $this->assertEquals($expected, $testclass->eval_cron_field($field, $min, $max));
75
    }
76
 
11 efrain 77
    public function test_get_next_scheduled_time(): void {
1 efrain 78
        global $CFG;
79
        $this->resetAfterTest();
80
 
81
        $this->setTimezone('Europe/London');
82
 
83
        // Let's specify the hour we are going to use initially for the test.
84
        // (note that we pick 01:00 that is tricky for Europe/London, because
85
        // it's exactly the Daylight Saving Time Begins hour.
86
        $testhour = 1;
87
 
88
        // Test job run at 1 am.
89
        $testclass = new scheduled_test_task();
90
 
91
        // All fields default to '*'.
92
        $testclass->set_hour($testhour);
93
        $testclass->set_minute('0');
94
        // Next valid time should be 1am of the next day.
95
        $nexttime = $testclass->get_next_scheduled_time();
96
 
97
        $oneamdate = new \DateTime('now', new \DateTimeZone('Europe/London'));
98
        $oneamdate->setTime($testhour, 0, 0);
99
 
100
        // Once a year (currently last Sunday of March), when changing to Daylight Saving Time,
101
        // Europe/London 01:00 simply doesn't exists because, exactly at 01:00 the clock
102
        // is advanced by one hour and becomes 02:00. When that happens, the DateInterval
103
        // calculations cannot be to advance by 1 day, but by one less hour. That is exactly when
104
        // the next scheduled run will happen (next day 01:00).
105
        $isdaylightsaving = false;
106
        if ($testhour < (int)$oneamdate->format('H')) {
107
            $isdaylightsaving = true;
108
        }
109
 
110
        // Make it 1 am tomorrow if the time is after 1am.
111
        if ($oneamdate->getTimestamp() < time()) {
112
            $oneamdate->add(new \DateInterval('P1D'));
113
            if ($isdaylightsaving) {
114
                // If today is Europe/London Daylight Saving Time Begins, expectation is 1 less hour.
115
                $oneamdate->sub(new \DateInterval('PT1H'));
116
            }
117
        }
118
        $oneam = $oneamdate->getTimestamp();
119
 
120
        $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
121
 
122
        // Disabled flag does not affect next time.
123
        $testclass->set_disabled(true);
124
        $nexttime = $testclass->get_next_scheduled_time();
125
        $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.');
126
 
127
        // Now test for job run every 10 minutes.
128
        $testclass = new scheduled_test_task();
129
 
130
        // All fields default to '*'.
131
        $testclass->set_minute('*/10');
132
        // Next valid time should be next 10 minute boundary.
133
        $nexttime = $testclass->get_next_scheduled_time();
134
 
135
        $minutes = ((intval(date('i') / 10)) + 1) * 10;
136
        $nexttenminutes = mktime(date('H'), $minutes, 0);
137
 
138
        $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
139
 
140
        // Disabled flag does not affect next time.
141
        $testclass->set_disabled(true);
142
        $nexttime = $testclass->get_next_scheduled_time();
143
        $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.');
144
 
145
        // Test hourly job executed on Sundays only.
146
        $testclass = new scheduled_test_task();
147
        $testclass->set_minute('0');
148
        $testclass->set_day_of_week('7');
149
 
150
        $nexttime = $testclass->get_next_scheduled_time();
151
 
152
        $this->assertEquals(7, date('N', $nexttime));
153
        $this->assertEquals(0, date('i', $nexttime));
154
 
155
        // Test monthly job.
156
        $testclass = new scheduled_test_task();
157
        $testclass->set_minute('32');
158
        $testclass->set_hour('0');
159
        $testclass->set_day('1');
160
 
161
        $nexttime = $testclass->get_next_scheduled_time();
162
 
163
        $this->assertEquals(32, date('i', $nexttime));
164
        $this->assertEquals(0, date('G', $nexttime));
165
        $this->assertEquals(1, date('j', $nexttime));
166
    }
167
 
168
    /**
169
     * Data provider for get_next_scheduled_time_detail.
170
     *
171
     * Note all times in here are in default Australia/Perth time zone.
172
     *
173
     * @return array[] Function parameters for each run
174
     */
175
    public static function get_next_scheduled_time_detail_provider(): array {
176
        return [
177
            // Every minute = next minute.
178
            ['2023-11-01 15:15', '*', '*', '*', '*', '*', '2023-11-01 15:16'],
179
            // Specified minute (coming up) = same hour, that minute.
180
            ['2023-11-01 15:15', '18', '*', '*', '*', '*', '2023-11-01 15:18'],
181
            // Specified minute (passed) = next hour, that minute.
182
            ['2023-11-01 15:15', '11', '*', '*', '*', '*', '2023-11-01 16:11'],
183
            // Range of minutes = same hour, next matching value.
184
            ['2023-11-01 15:15', '*/15', '*', '*', '*', '*', '2023-11-01 15:30'],
185
            // Specified hour, any minute = first minute that hour.
186
            ['2023-11-01 15:15', '*', '20', '*', '*', '*', '2023-11-01 20:00'],
187
            // Specified hour, specified minute = that time.
188
            ['2023-11-01 15:15', '13', '20', '*', '*', '*', '2023-11-01 20:13'],
189
            // Any minute, range of hours = next hour in range, 00:00.
190
            ['2023-11-01 15:15', '*', '*/6', '*', '*', '*', '2023-11-01 18:00'],
191
            // Specified minute, range of hours = next hour where minute not passed, that minute.
192
            ['2023-11-01 18:15', '10', '*/6', '*', '*', '*', '2023-11-02 00:10'],
193
            // Specified day, any hour/minute.
194
            ['2023-11-01 15:15', '*', '*', '3', '*', '*', '2023-11-03 00:00'],
195
            // Specified day (next month), any hour/minute.
196
            ['2023-11-05 15:15', '*', '*', '3', '*', '*', '2023-12-03 00:00'],
197
            // Specified day, specified hour.
198
            ['2023-11-01 15:15', '*', '17', '3', '*', '*', '2023-11-03 17:00'],
199
            // Specified day, specified minute.
200
            ['2023-11-01 15:15', '17', '*', '3', '*', '*', '2023-11-03 00:17'],
201
            // 30th of every month, February.
202
            ['2023-01-31 15:15', '15', '10', '30', '*', '*', '2023-03-30 10:15'],
203
            // Friday, any time. 2023-11-01 is a Wednesday, so it will run in 2 days.
204
            ['2023-11-01 15:15', '*', '*', '*', '5', '*', '2023-11-03 00:00'],
205
            // Friday, any time (but it's already Friday).
206
            ['2023-11-03 15:15', '*', '*', '*', '5', '*', '2023-11-03 15:16'],
207
            // Sunday (week rollover).
208
            ['2023-11-01 15:15', '*', '*', '*', '0', '*', '2023-11-05 00:00'],
209
            // Specified days and day of week (days come first).
210
            ['2023-11-01 15:15', '*', '*', '2,4,6', '5', '*', '2023-11-02 00:00'],
211
            // Specified days and day of week (day of week comes first).
212
            ['2023-11-01 15:15', '*', '*', '4,6,8', '5', '*', '2023-11-03 00:00'],
213
            // Specified months.
214
            ['2023-11-01 15:15', '*', '*', '*', '*', '6,8,10,12', '2023-12-01 00:00'],
215
            // Specified months (crossing year).
216
            ['2023-11-01 15:15', '*', '*', '*', '*', '6,8,10', '2024-06-01 00:00'],
217
            // Specified months and day of week (i.e. first Sunday in December).
218
            ['2023-11-01 15:15', '*', '*', '*', '0', '6,8,10,12', '2023-12-03 00:00'],
219
            // It's already December, but the next Friday is not until next month.
220
            ['2023-12-30 15:15', '*', '*', '*', '5', '6,8,10,12', '2024-06-07 00:00'],
221
            // Around end of year.
222
            ['2023-12-31 23:00', '10', '3', '*', '*', '*', '2024-01-01 03:10'],
223
            // Some impossible requirements...
224
            ['2023-12-31 23:00', '*', '*', '30', '*', '2', scheduled_task::NEVER_RUN_TIME],
225
            ['2023-12-31 23:00', '*', '*', '31', '*', '9,4,6,11', scheduled_task::NEVER_RUN_TIME],
226
            // Normal years and leap years.
227
            ['2021-01-01 23:00', '*', '*', '28', '*', '2', '2021-02-28 00:00'],
228
            ['2021-01-01 23:00', '*', '*', '29', '*', '2', '2024-02-29 00:00'],
229
            // Missing leap year over century. Longest possible gap between runs.
230
            ['2096-03-01 00:00', '59', '23', '29', '*', '2', '2104-02-29 23:59'],
231
        ];
232
    }
233
 
234
    /**
235
     * Tests get_next_scheduled_time using a large number of example scenarios.
236
     *
237
     * @param string $now Current time (strtotime format)
238
     * @param string $minute Minute restriction list for task
239
     * @param string $hour Hour restriction list for task
240
     * @param string $day Day restriction list for task
241
     * @param string $dayofweek Day of week restriction list for task
242
     * @param string $month Month restriction list for task
243
     * @param string|int $expected Expected run time (strtotime format or time int)
244
     * @dataProvider get_next_scheduled_time_detail_provider
245
     * @covers ::get_next_scheduled_time
246
     */
247
    public function test_get_next_scheduled_time_detail(string $now, string $minute, string $hour,
248
            string $day, string $dayofweek, string $month, string|int $expected): void {
249
        // Create test task with specified times.
250
        $task = new scheduled_test_task();
251
        $task->set_minute($minute);
252
        $task->set_hour($hour);
253
        $task->set_day($day);
254
        $task->set_day_of_week($dayofweek);
255
        $task->set_month($month);
256
 
257
        // Check function results.
258
        $nowtime = strtotime($now);
259
        if (is_int($expected)) {
260
            $expectedtime = $expected;
261
        } else {
262
            $expectedtime = strtotime($expected);
263
        }
264
        $actualtime = $task->get_next_scheduled_time($nowtime);
265
        $this->assertEquals($expectedtime, $actualtime, 'Expected ' . $expected . ', actual ' . date('Y-m-d H:i', $actualtime));
266
    }
267
 
268
    /**
269
     * Tests get_next_scheduled_time around DST changes, with regard to the continuity of frequent
270
     * tasks.
271
     *
272
     * We want frequent tasks to keep progressing as normal and not randomly stop for an hour, or
273
     * suddenly decide they need to happen in the past.
274
     *
275
     * @covers ::get_next_scheduled_time
276
     */
277
    public function test_get_next_scheduled_time_dst_continuity(): void {
278
        $this->resetAfterTest();
279
        $this->setTimezone('Europe/London');
280
 
281
        // Test task is set to run every 20 minutes (:00, :20, :40).
282
        $task = new scheduled_test_task();
283
        $task->set_minute('*/20');
284
 
285
        // DST change forwards. Check times in GMT to ensure it progresses as normal.
286
        $before = strtotime('2023-03-26 00:59 GMT');
287
        $this->assertEquals(strtotime('2023-03-26 00:59 Europe/London'), $before);
288
        $one = $task->get_next_scheduled_time($before);
289
        $this->assertEquals(strtotime('2023-03-26 01:00 GMT'), $one);
290
        $this->assertEquals(strtotime('2023-03-26 02:00 Europe/London'), $one);
291
        $two = $task->get_next_scheduled_time($one);
292
        $this->assertEquals(strtotime('2023-03-26 01:20 GMT'), $two);
293
        $three = $task->get_next_scheduled_time($two);
294
        $this->assertEquals(strtotime('2023-03-26 01:40 GMT'), $three);
295
        $four = $task->get_next_scheduled_time($three);
296
        $this->assertEquals(strtotime('2023-03-26 02:00 GMT'), $four);
297
 
298
        // DST change backwards.
299
        $before = strtotime('2023-10-29 00:59 GMT');
300
        // The 'before' time is 01:59 Europe/London, but we won't explicitly test that because
301
        // there are two 01:59s so it might fail depending on implementation.
302
        $one = $task->get_next_scheduled_time($before);
303
        $this->assertEquals(strtotime('2023-10-29 01:00 GMT'), $one);
304
        // We cannot compare against the Eerope/London time (01:00) because there are two 01:00s.
305
        $two = $task->get_next_scheduled_time($one);
306
        $this->assertEquals(strtotime('2023-10-29 01:20 GMT'), $two);
307
        $three = $task->get_next_scheduled_time($two);
308
        $this->assertEquals(strtotime('2023-10-29 01:40 GMT'), $three);
309
        $four = $task->get_next_scheduled_time($three);
310
        $this->assertEquals(strtotime('2023-10-29 02:00 GMT'), $four);
311
        // This time is now unambiguous in Europe/London.
312
        $this->assertEquals(strtotime('2023-10-29 02:00 Europe/London'), $four);
313
    }
314
 
11 efrain 315
    public function test_timezones(): void {
1 efrain 316
        global $CFG, $USER;
317
 
318
        // The timezones used in this test are chosen because they do not use DST - that would break the test.
319
        $this->resetAfterTest();
320
 
321
        $this->setTimezone('Asia/Kabul');
322
 
323
        $testclass = new scheduled_test_task();
324
 
325
        // Scheduled tasks should always use servertime - so this is 03:30 GMT.
326
        $testclass->set_hour('1');
327
        $testclass->set_minute('0');
328
 
329
        // Next valid time should be 1am of the next day.
330
        $nexttime = $testclass->get_next_scheduled_time();
331
 
332
        // GMT+05:45.
333
        $USER->timezone = 'Asia/Kathmandu';
334
        $userdate = userdate($nexttime);
335
 
336
        // Should be displayed in user timezone.
337
        // I used http://www.timeanddate.com/worldclock/fixedtime.html?msg=Moodle+Test&iso=20160502T01&p1=113
338
        // setting my location to Kathmandu to verify this time.
339
        $this->assertStringContainsString('2:15 AM', \core_text::strtoupper($userdate));
340
    }
341
 
342
    public function test_reset_scheduled_tasks_for_component_customised(): void {
343
        $this->resetAfterTest(true);
344
 
345
        $tasks = manager::load_scheduled_tasks_for_component('moodle');
346
 
347
        // Customise a task.
348
        $task = reset($tasks);
349
        $task->set_minute('1');
350
        $task->set_hour('2');
351
        $task->set_day('3');
352
        $task->set_month('4');
353
        $task->set_day_of_week('5');
354
        $task->set_customised('1');
355
        manager::configure_scheduled_task($task);
356
 
357
        // Now call reset.
358
        manager::reset_scheduled_tasks_for_component('moodle');
359
 
360
        // Fetch the task again.
361
        $taskafterreset = manager::get_scheduled_task(get_class($task));
362
 
363
        // The task should still be the same as the customised.
364
        $this->assertTaskEquals($task, $taskafterreset);
365
    }
366
 
367
    public function test_reset_scheduled_tasks_for_component_deleted(): void {
368
        global $DB;
369
        $this->resetAfterTest(true);
370
 
371
        // Delete a task to simulate the fact that its new.
372
        $tasklist = manager::load_scheduled_tasks_for_component('moodle');
373
 
374
        // Note: This test must use a task which does not use any random values.
375
        $task = manager::get_scheduled_task(session_cleanup_task::class);
376
 
377
        $DB->delete_records('task_scheduled', array('classname' => '\\' . trim(get_class($task), '\\')));
378
        $this->assertFalse(manager::get_scheduled_task(session_cleanup_task::class));
379
 
380
        // Now call reset on all the tasks.
381
        manager::reset_scheduled_tasks_for_component('moodle');
382
 
383
        // Assert that the second task was added back.
384
        $taskafterreset = manager::get_scheduled_task(session_cleanup_task::class);
385
        $this->assertNotFalse($taskafterreset);
386
 
387
        $this->assertTaskEquals($task, $taskafterreset);
388
        $this->assertCount(count($tasklist), manager::load_scheduled_tasks_for_component('moodle'));
389
    }
390
 
391
    public function test_reset_scheduled_tasks_for_component_changed_in_source(): void {
392
        $this->resetAfterTest(true);
393
 
394
        // Delete a task to simulate the fact that its new.
395
        // Note: This test must use a task which does not use any random values.
396
        $task = manager::get_scheduled_task(session_cleanup_task::class);
397
 
398
        // Get a copy of the task before maing changes for later comparison.
399
        $taskbeforechange = manager::get_scheduled_task(session_cleanup_task::class);
400
 
401
        // Edit a task to simulate a change in its definition (as if it was not customised).
402
        $task->set_minute('1');
403
        $task->set_hour('2');
404
        $task->set_day('3');
405
        $task->set_month('4');
406
        $task->set_day_of_week('5');
407
        manager::configure_scheduled_task($task);
408
 
409
        // Fetch the task out for comparison.
410
        $taskafterchange = manager::get_scheduled_task(session_cleanup_task::class);
411
 
412
        // The task should now be different to the original.
413
        $this->assertTaskNotEquals($taskbeforechange, $taskafterchange);
414
 
415
        // Now call reset.
416
        manager::reset_scheduled_tasks_for_component('moodle');
417
 
418
        // Fetch the task again.
419
        $taskafterreset = manager::get_scheduled_task(session_cleanup_task::class);
420
 
421
        // The task should now be the same as the original.
422
        $this->assertTaskEquals($taskbeforechange, $taskafterreset);
423
    }
424
 
425
    /**
426
     * Tests that the reset function deletes old tasks.
427
     */
11 efrain 428
    public function test_reset_scheduled_tasks_for_component_delete(): void {
1 efrain 429
        global $DB;
430
        $this->resetAfterTest(true);
431
 
432
        $count = $DB->count_records('task_scheduled', array('component' => 'moodle'));
433
        $allcount = $DB->count_records('task_scheduled');
434
 
435
        $task = new scheduled_test_task();
436
        $task->set_component('moodle');
437
        $record = manager::record_from_scheduled_task($task);
438
        $DB->insert_record('task_scheduled', $record);
439
        $this->assertTrue($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test_task',
440
            'component' => 'moodle')));
441
 
442
        $task = new scheduled_test2_task();
443
        $task->set_component('moodle');
444
        $record = manager::record_from_scheduled_task($task);
445
        $DB->insert_record('task_scheduled', $record);
446
        $this->assertTrue($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test2_task',
447
            'component' => 'moodle')));
448
 
449
        $aftercount = $DB->count_records('task_scheduled', array('component' => 'moodle'));
450
        $afterallcount = $DB->count_records('task_scheduled');
451
 
452
        $this->assertEquals($count + 2, $aftercount);
453
        $this->assertEquals($allcount + 2, $afterallcount);
454
 
455
        // Now check that the right things were deleted.
456
        manager::reset_scheduled_tasks_for_component('moodle');
457
 
458
        $this->assertEquals($count, $DB->count_records('task_scheduled', array('component' => 'moodle')));
459
        $this->assertEquals($allcount, $DB->count_records('task_scheduled'));
460
        $this->assertFalse($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test2_task',
461
            'component' => 'moodle')));
462
        $this->assertFalse($DB->record_exists('task_scheduled', array('classname' => '\core\task\scheduled_test_task',
463
            'component' => 'moodle')));
464
    }
465
 
11 efrain 466
    public function test_get_next_scheduled_task(): void {
1 efrain 467
        global $DB;
468
 
469
        $this->resetAfterTest(true);
470
        // Delete all existing scheduled tasks.
471
        $DB->delete_records('task_scheduled');
472
        // Add a scheduled task.
473
 
474
        // A task that runs once per hour.
475
        $record = new \stdClass();
476
        $record->blocking = true;
477
        $record->minute = '0';
478
        $record->hour = '0';
479
        $record->dayofweek = '*';
480
        $record->day = '*';
481
        $record->month = '*';
482
        $record->component = 'test_scheduled_task';
483
        $record->classname = '\core\task\scheduled_test_task';
484
 
485
        $DB->insert_record('task_scheduled', $record);
486
        // And another one to test failures.
487
        $record->classname = '\core\task\scheduled_test2_task';
488
        $DB->insert_record('task_scheduled', $record);
489
        // And disabled test.
490
        $record->classname = '\core\task\scheduled_test3_task';
491
        $record->disabled = 1;
492
        $DB->insert_record('task_scheduled', $record);
493
 
494
        $now = time();
495
 
496
        // Should get handed the first task.
497
        $task = manager::get_next_scheduled_task($now);
498
        $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
499
        $task->execute();
500
 
501
        manager::scheduled_task_complete($task);
502
        // Should get handed the second task.
503
        $task = manager::get_next_scheduled_task($now);
504
        $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
505
        $task->execute();
506
 
507
        manager::scheduled_task_failed($task);
508
        // Should not get any task.
509
        $task = manager::get_next_scheduled_task($now);
510
        $this->assertNull($task);
511
 
512
        // Should get the second task (retry after delay).
513
        $task = manager::get_next_scheduled_task($now + 120);
514
        $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
515
        $task->execute();
516
 
517
        manager::scheduled_task_complete($task);
518
 
519
        // Should not get any task.
520
        $task = manager::get_next_scheduled_task($now);
521
        $this->assertNull($task);
522
 
523
        // Check ordering.
524
        $DB->delete_records('task_scheduled');
525
        $record->lastruntime = 2;
526
        $record->disabled = 0;
527
        $record->classname = '\core\task\scheduled_test_task';
528
        $DB->insert_record('task_scheduled', $record);
529
 
530
        $record->lastruntime = 1;
531
        $record->classname = '\core\task\scheduled_test2_task';
532
        $DB->insert_record('task_scheduled', $record);
533
 
534
        // Should get handed the second task.
535
        $task = manager::get_next_scheduled_task($now);
536
        $this->assertInstanceOf('\core\task\scheduled_test2_task', $task);
537
        $task->execute();
538
        manager::scheduled_task_complete($task);
539
 
540
        // Should get handed the first task.
541
        $task = manager::get_next_scheduled_task($now);
542
        $this->assertInstanceOf('\core\task\scheduled_test_task', $task);
543
        $task->execute();
544
        manager::scheduled_task_complete($task);
545
 
546
        // Should not get any task.
547
        $task = manager::get_next_scheduled_task($now);
548
        $this->assertNull($task);
549
    }
550
 
11 efrain 551
    public function test_get_broken_scheduled_task(): void {
1 efrain 552
        global $DB;
553
 
554
        $this->resetAfterTest(true);
555
        // Delete all existing scheduled tasks.
556
        $DB->delete_records('task_scheduled');
557
        // Add a scheduled task.
558
 
559
        // A broken task that runs all the time.
560
        $record = new \stdClass();
561
        $record->blocking = true;
562
        $record->minute = '*';
563
        $record->hour = '*';
564
        $record->dayofweek = '*';
565
        $record->day = '*';
566
        $record->month = '*';
567
        $record->component = 'test_scheduled_task';
568
        $record->classname = '\core\task\scheduled_test_task_broken';
569
 
570
        $DB->insert_record('task_scheduled', $record);
571
 
572
        $now = time();
573
        // Should not get any task.
574
        $task = manager::get_next_scheduled_task($now);
575
        $this->assertDebuggingCalled();
576
        $this->assertNull($task);
577
    }
578
 
579
    /**
580
     * Tests the use of 'R' syntax in time fields of tasks to get
581
     * tasks be configured with a non-uniform time.
582
     */
11 efrain 583
    public function test_random_time_specification(): void {
1 efrain 584
 
585
        // Testing non-deterministic things in a unit test is not really
586
        // wise, so we just test the values have changed within allowed bounds.
587
        $testclass = new scheduled_test_task();
588
 
589
        // The test task defaults to '*'.
590
        $this->assertIsString($testclass->get_minute());
591
        $this->assertIsString($testclass->get_hour());
592
 
593
        // Set a random value.
594
        $testclass->set_minute('R');
595
        $testclass->set_hour('R');
596
        $testclass->set_day_of_week('R');
597
 
598
        // Verify the minute has changed within allowed bounds.
599
        $minute = $testclass->get_minute();
600
        $this->assertIsInt($minute);
601
        $this->assertGreaterThanOrEqual(0, $minute);
602
        $this->assertLessThanOrEqual(59, $minute);
603
 
604
        // Verify the hour has changed within allowed bounds.
605
        $hour = $testclass->get_hour();
606
        $this->assertIsInt($hour);
607
        $this->assertGreaterThanOrEqual(0, $hour);
608
        $this->assertLessThanOrEqual(23, $hour);
609
 
610
        // Verify the dayofweek has changed within allowed bounds.
611
        $dayofweek = $testclass->get_day_of_week();
612
        $this->assertIsInt($dayofweek);
613
        $this->assertGreaterThanOrEqual(0, $dayofweek);
614
        $this->assertLessThanOrEqual(6, $dayofweek);
615
    }
616
 
617
    /**
618
     * Test that the file_temp_cleanup_task removes directories and
619
     * files as expected.
620
     */
11 efrain 621
    public function test_file_temp_cleanup_task(): void {
1 efrain 622
        global $CFG;
623
        $backuptempdir = make_backup_temp_directory('');
624
 
625
        // Create directories.
626
        $dir = $backuptempdir . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR . 'courses';
627
        mkdir($dir, 0777, true);
628
 
629
        // Create files to be checked and then deleted.
630
        $file01 = $dir . DIRECTORY_SEPARATOR . 'sections.xml';
631
        file_put_contents($file01, 'test data 001');
632
        $file02 = $dir . DIRECTORY_SEPARATOR . 'modules.xml';
633
        file_put_contents($file02, 'test data 002');
634
        // Change the time modified for the first file, to a time that will be deleted by the task (greater than seven days).
635
        touch($file01, time() - (8 * 24 * 3600));
636
 
637
        $task = manager::get_scheduled_task('\\core\\task\\file_temp_cleanup_task');
638
        $this->assertInstanceOf('\core\task\file_temp_cleanup_task', $task);
639
        $task->execute();
640
 
641
        // Scan the directory. Only modules.xml should be left.
642
        $filesarray = scandir($dir);
643
        $this->assertEquals('modules.xml', $filesarray[2]);
644
        $this->assertEquals(3, count($filesarray));
645
 
646
        // Change the time modified on modules.xml.
647
        touch($file02, time() - (8 * 24 * 3600));
648
        // Change the time modified on the courses directory.
649
        touch($backuptempdir . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR .
650
                'courses', time() - (8 * 24 * 3600));
651
        // Run the scheduled task to remove the file and directory.
652
        $task->execute();
653
        $filesarray = scandir($backuptempdir . DIRECTORY_SEPARATOR . 'backup01');
654
        // There should only be two items in the array, '.' and '..'.
655
        $this->assertEquals(2, count($filesarray));
656
 
657
        // Change the time modified on all of the files and directories.
658
        $dir = new \RecursiveDirectoryIterator($CFG->tempdir);
659
        // Show all child nodes prior to their parent.
660
        $iter = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST);
661
 
662
        for ($iter->rewind(); $iter->valid(); $iter->next()) {
663
            if ($iter->isDir() && !$iter->isDot()) {
664
                $node = $iter->getRealPath();
665
                touch($node, time() - (8 * 24 * 3600));
666
            }
667
        }
668
 
669
        // Run the scheduled task again to remove all of the files and directories.
670
        $task->execute();
671
        $filesarray = scandir($CFG->tempdir);
672
        // All of the files and directories should be deleted.
673
        // There should only be three items in the array, '.', '..' and '.htaccess'.
674
        $this->assertEquals([ '.', '..', '.htaccess' ], $filesarray);
675
    }
676
 
677
    /**
678
     * Test that the function to clear the fail delay from a task works correctly.
679
     */
11 efrain 680
    public function test_clear_fail_delay(): void {
1 efrain 681
 
682
        $this->resetAfterTest();
683
 
684
        // Get an example task to use for testing. Task is set to run every minute by default.
685
        $taskname = '\core\task\send_new_user_passwords_task';
686
 
687
        // Pretend task started running and then failed 3 times.
688
        $before = time();
689
        $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
690
        for ($i = 0; $i < 3; $i ++) {
691
            $task = manager::get_scheduled_task($taskname);
692
            $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10);
693
            $task->set_lock($lock);
694
            manager::scheduled_task_failed($task);
695
        }
696
 
697
        // Confirm task is now delayed by several minutes.
698
        $task = manager::get_scheduled_task($taskname);
699
        $this->assertEquals(240, $task->get_fail_delay());
700
        $this->assertGreaterThan($before + 230, $task->get_next_run_time());
701
 
702
        // Clear the fail delay and re-get the task.
703
        manager::clear_fail_delay($task);
704
        $task = manager::get_scheduled_task($taskname);
705
 
706
        // There should be no delay and it should run within the next minute.
707
        $this->assertEquals(0, $task->get_fail_delay());
708
        $this->assertLessThan($before + 70, $task->get_next_run_time());
709
    }
710
 
711
    /**
712
     * Data provider for test_scheduled_task_override_values.
713
     */
714
    public static function provider_schedule_overrides(): array {
715
        return array(
716
            array(
717
                'scheduled_tasks' => array(
718
                    '\core\task\scheduled_test_task' => array(
719
                        'schedule' => '10 13 1 2 4',
720
                        'disabled' => 0,
721
                    ),
722
                    '\core\task\scheduled_test2_task' => array(
723
                        'schedule' => '* * * * *',
724
                        'disabled' => 1,
725
                    ),
726
                ),
727
                'task_full_classnames' => array(
728
                    '\core\task\scheduled_test_task',
729
                    '\core\task\scheduled_test2_task',
730
                ),
731
                'expected' => array(
732
                    '\core\task\scheduled_test_task' => array(
733
                        'min'   => '10',
734
                        'hour'  => '13',
735
                        'day'   => '1',
736
                        'month' => '2',
737
                        'week'  => '4',
738
                        'disabled' => 0,
739
                    ),
740
                    '\core\task\scheduled_test2_task' => array(
741
                        'min'   => '*',
742
                        'hour'  => '*',
743
                        'day'   => '*',
744
                        'month' => '*',
745
                        'week'  => '*',
746
                        'disabled' => 1,
747
                    ),
748
                )
749
            ),
750
            array(
751
                'scheduled_tasks' => array(
752
                    '\core\task\*' => array(
753
                        'schedule' => '1 2 3 4 5',
754
                        'disabled' => 0,
755
                    )
756
                ),
757
                'task_full_classnames' => array(
758
                    '\core\task\scheduled_test_task',
759
                    '\core\task\scheduled_test2_task',
760
                ),
761
                'expected' => array(
762
                    '\core\task\scheduled_test_task' => array(
763
                        'min'   => '1',
764
                        'hour'  => '2',
765
                        'day'   => '3',
766
                        'month' => '4',
767
                        'week'  => '5',
768
                        'disabled' => 0,
769
                    ),
770
                    '\core\task\scheduled_test2_task' => array(
771
                        'min'   => '1',
772
                        'hour'  => '2',
773
                        'day'   => '3',
774
                        'month' => '4',
775
                        'week'  => '5',
776
                        'disabled' => 0,
777
                    ),
778
                )
779
            )
780
        );
781
    }
782
 
783
 
784
    /**
785
     * Test to ensure scheduled tasks are updated by values set in config.
786
     *
787
     * @param array $overrides
788
     * @param array $tasks
789
     * @param array $expected
790
     * @dataProvider provider_schedule_overrides
791
     */
792
    public function test_scheduled_task_override_values(array $overrides, array $tasks, array $expected): void {
793
        global $CFG, $DB;
794
 
795
        $this->resetAfterTest();
796
 
797
        // Add overrides to the config.
798
        $CFG->scheduled_tasks = $overrides;
799
 
800
        // Set up test scheduled task record.
801
        $record = new \stdClass();
802
        $record->component = 'test_scheduled_task';
803
 
804
        foreach ($tasks as $task) {
805
            $record->classname = $task;
806
            $DB->insert_record('task_scheduled', $record);
807
 
808
            $scheduledtask = manager::get_scheduled_task($task);
809
            $expectedresults = $expected[$task];
810
 
811
            // Check that the task is actually overridden.
812
            $this->assertTrue($scheduledtask->is_overridden(), 'Is overridden');
813
 
814
            // Check minute is correct.
815
            $this->assertEquals($expectedresults['min'], $scheduledtask->get_minute(), 'Minute check');
816
 
817
            // Check day is correct.
818
            $this->assertEquals($expectedresults['day'], $scheduledtask->get_day(), 'Day check');
819
 
820
            // Check hour is correct.
821
            $this->assertEquals($expectedresults['hour'], $scheduledtask->get_hour(), 'Hour check');
822
 
823
            // Check week is correct.
824
            $this->assertEquals($expectedresults['week'], $scheduledtask->get_day_of_week(), 'Day of week check');
825
 
826
            // Check week is correct.
827
            $this->assertEquals($expectedresults['month'], $scheduledtask->get_month(), 'Month check');
828
 
829
            // Check to see if the task is disabled.
830
            $this->assertEquals($expectedresults['disabled'], $scheduledtask->get_disabled(), 'Disabled check');
831
        }
832
    }
833
 
834
    /**
835
     * Check that an overridden task is sent to be processed.
836
     */
837
    public function test_scheduled_task_overridden_task_can_run(): void {
838
        global $CFG, $DB;
839
 
840
        $this->resetAfterTest();
841
 
842
        // Delete all existing scheduled tasks.
843
        $DB->delete_records('task_scheduled');
844
 
845
        // Add overrides to the config.
846
        $CFG->scheduled_tasks = [
847
            '\core\task\scheduled_test_task' => [
848
                'disabled' => 1
849
            ],
850
            '\core\task\scheduled_test2_task' => [
851
                'disabled' => 0
852
            ],
853
        ];
854
 
855
        // A task that runs once per hour.
856
        $record = new \stdClass();
857
        $record->component = 'test_scheduled_task';
858
        $record->classname = '\core\task\scheduled_test_task';
859
        $record->disabled = 0;
860
        $DB->insert_record('task_scheduled', $record);
861
 
862
        // And disabled test.
863
        $record->classname = '\core\task\scheduled_test2_task';
864
        $record->disabled = 1;
865
        $DB->insert_record('task_scheduled', $record);
866
 
867
        $now = time();
868
 
869
        $scheduledtask = manager::get_next_scheduled_task($now);
870
        $this->assertInstanceOf('\core\task\scheduled_test2_task', $scheduledtask);
871
        $scheduledtask->execute();
872
        manager::scheduled_task_complete($scheduledtask);
873
    }
874
 
875
    /**
876
     * Assert that the specified tasks are equal.
877
     *
878
     * @param   \core\task\task_base $task
879
     * @param   \core\task\task_base $comparisontask
880
     */
881
    public function assertTaskEquals(task_base $task, task_base $comparisontask): void {
882
        // Convert both to an object.
883
        $task = manager::record_from_scheduled_task($task);
884
        $comparisontask = manager::record_from_scheduled_task($comparisontask);
885
 
886
        // Reset the nextruntime field as it is intentionally dynamic.
887
        $task->nextruntime = null;
888
        $comparisontask->nextruntime = null;
889
 
890
        $args = array_merge(
891
            [
892
                $task,
893
                $comparisontask,
894
            ],
895
            array_slice(func_get_args(), 2)
896
        );
897
 
898
        call_user_func_array([$this, 'assertEquals'], $args);
899
    }
900
 
901
    /**
902
     * Assert that the specified tasks are not equal.
903
     *
904
     * @param   \core\task\task_base $task
905
     * @param   \core\task\task_base $comparisontask
906
     */
907
    public function assertTaskNotEquals(task_base $task, task_base $comparisontask): void {
908
        // Convert both to an object.
909
        $task = manager::record_from_scheduled_task($task);
910
        $comparisontask = manager::record_from_scheduled_task($comparisontask);
911
 
912
        // Reset the nextruntime field as it is intentionally dynamic.
913
        $task->nextruntime = null;
914
        $comparisontask->nextruntime = null;
915
 
916
        $args = array_merge(
917
            [
918
                $task,
919
                $comparisontask,
920
            ],
921
            array_slice(func_get_args(), 2)
922
        );
923
 
924
        call_user_func_array([$this, 'assertNotEquals'], $args);
925
    }
926
 
927
    /**
928
     * Assert that the lastruntime column holds an original value after a scheduled task is reset.
929
     */
930
    public function test_reset_scheduled_tasks_for_component_keeps_original_lastruntime(): void {
931
        global $DB;
932
        $this->resetAfterTest(true);
933
 
934
        // Set lastruntime for the scheduled task.
935
        $DB->set_field('task_scheduled', 'lastruntime', 123456789, ['classname' => '\core\task\session_cleanup_task']);
936
 
937
        // Reset the task.
938
        manager::reset_scheduled_tasks_for_component('moodle');
939
 
940
        // Fetch the task again.
941
        $taskafterreset = manager::get_scheduled_task(session_cleanup_task::class);
942
 
943
        // Confirm, that lastruntime is still in place.
944
        $this->assertEquals(123456789, $taskafterreset->get_last_run_time());
945
    }
946
 
947
    /**
948
     * Data provider for {@see test_is_component_enabled}
949
     *
950
     * @return array[]
951
     */
952
    public function is_component_enabled_provider(): array {
953
        return [
954
            'Enabled component' => ['auth_cas', true],
955
            'Disabled component' => ['auth_ldap', false],
956
            'Invalid component' => ['auth_invalid', false],
957
        ];
958
    }
959
 
960
    /**
961
     * Tests whether tasks belonging to components consider the component to be enabled
962
     *
963
     * @param string $component
964
     * @param bool $expected
965
     *
966
     * @dataProvider is_component_enabled_provider
967
     */
968
    public function test_is_component_enabled(string $component, bool $expected): void {
969
        $this->resetAfterTest();
970
 
971
        // Set cas as the only enabled auth component.
972
        set_config('auth', 'cas');
973
 
974
        $task = new scheduled_test_task();
975
        $task->set_component($component);
976
 
977
        $this->assertEquals($expected, $task->is_component_enabled());
978
    }
979
 
980
    /**
981
     * Test whether tasks belonging to core components considers the component to be enabled
982
     */
983
    public function test_is_component_enabled_core(): void {
984
        $task = new scheduled_test_task();
985
        $this->assertTrue($task->is_component_enabled());
986
    }
987
 
988
    /**
989
     * Test disabling and enabling individual tasks.
990
     *
991
     * @covers ::disable
992
     * @covers ::enable
993
     * @covers ::has_default_configuration
994
     */
995
    public function test_disable_and_enable_task(): void {
996
        $this->resetAfterTest();
997
 
998
        // We use a real task because the manager doesn't know about the test tasks.
999
        $taskname = '\core\task\send_new_user_passwords_task';
1000
 
1001
        $task = manager::get_scheduled_task($taskname);
1002
        $defaulttask = manager::get_default_scheduled_task($taskname);
1003
        $this->assertTaskEquals($task, $defaulttask);
1004
 
1005
        // Disable task and verify drift.
1006
        $task->disable();
1007
        $this->assertTaskNotEquals($task, $defaulttask);
1008
        $this->assertEquals(1, $task->get_disabled());
1009
        $this->assertEquals(false, $task->has_default_configuration());
1010
 
1011
        // Enable task and verify not drifted.
1012
        $task->enable();
1013
        $this->assertTaskEquals($task, $defaulttask);
1014
        $this->assertEquals(0, $task->get_disabled());
1015
        $this->assertEquals(true, $task->has_default_configuration());
1016
 
1017
        // Modify task and verify drift.
1018
        $task->set_hour(1);
1019
        \core\task\manager::configure_scheduled_task($task);
1020
        $this->assertTaskNotEquals($task, $defaulttask);
1021
        $this->assertEquals(1, $task->get_hour());
1022
        $this->assertEquals(false, $task->has_default_configuration());
1023
 
1024
        // Disable task and verify drift.
1025
        $task->disable();
1026
        $this->assertTaskNotEquals($task, $defaulttask);
1027
        $this->assertEquals(1, $task->get_disabled());
1028
        $this->assertEquals(1, $task->get_hour());
1029
        $this->assertEquals(false, $task->has_default_configuration());
1030
 
1031
        // Enable task and verify drift.
1032
        $task->enable();
1033
        $this->assertTaskNotEquals($task, $defaulttask);
1034
        $this->assertEquals(0, $task->get_disabled());
1035
        $this->assertEquals(1, $task->get_hour());
1036
        $this->assertEquals(false, $task->has_default_configuration());
1037
    }
1038
 
1039
    /**
1040
     * Test send messages when a task reaches the max fail delay time.
1041
     *
1042
     * @covers ::scheduled_task_failed
1043
     * @covers ::send_failed_task_max_delay_message
1044
     */
1045
    public function test_message_max_fail_delay(): void {
1046
        $this->resetAfterTest();
1047
        $this->setAdminUser();
1048
 
1049
        // Redirect messages.
1050
        $messagesink = $this->redirectMessages();
1051
        $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
1052
 
1053
        // Get an example task to use for testing. Task is set to run every minute by default.
1054
        $taskname = '\core\task\send_new_user_passwords_task';
1055
        $task = manager::get_scheduled_task($taskname);
1056
        $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10);
1057
        $task->set_lock($lock);
1058
        // Catch the message.
1059
        manager::scheduled_task_failed($task);
1060
        $messages = $messagesink->get_messages();
1061
        $this->assertCount(0, $messages);
1062
 
1063
        // Set the max fail delay time.
1064
        $task = manager::get_scheduled_task($taskname);
1065
        $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10);
1066
        $task->set_lock($lock);
1067
        $task->set_fail_delay(86400);
1068
        $task->execute();
1069
        // Catch the message.
1070
        manager::scheduled_task_failed($task);
1071
        $messages = $messagesink->get_messages();
1072
        $this->assertCount(1, $messages);
1073
 
1074
        // Get the task and execute it second time.
1075
        $task = manager::get_scheduled_task($taskname);
1076
        $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10);
1077
        $task->set_lock($lock);
1078
        // Set the fail delay to 12 hours.
1079
        $task->set_fail_delay(43200);
1080
        $task->execute();
1081
        manager::scheduled_task_failed($task);
1082
        // Catch the message.
1083
        $messages = $messagesink->get_messages();
1084
        $this->assertCount(2, $messages);
1085
 
1086
        // Get the task and execute it third time.
1087
        $task = manager::get_scheduled_task($taskname);
1088
        $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10);
1089
        $task->set_lock($lock);
1090
        // Set the fail delay to 48 hours.
1091
        $task->set_fail_delay(172800);
1092
        $task->execute();
1093
        manager::scheduled_task_failed($task);
1094
        // Catch the message.
1095
        $messages = $messagesink->get_messages();
1096
        $this->assertCount(3, $messages);
1097
 
1098
        // Check first message information.
1099
        $this->assertStringContainsString('Task failed: Send new user passwords', $messages[0]->subject);
1100
        $this->assertEquals('failedtaskmaxdelay', $messages[0]->eventtype);
1101
        $this->assertEquals('-10', $messages[0]->useridfrom);
1102
        $this->assertEquals('2', $messages[0]->useridto);
1103
 
1104
        // Close sink.
1105
        $messagesink->close();
1106
    }
1107
}