MDL-49399 task: Add admin log viewer
[moodle.git] / lib / tests / task_logging_test.php
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/>.
17 /**
18  * This file contains the unit tests for the task logging system.
19  *
20  * @package   core
21  * @category  phpunit
22  * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
27 require_once(__DIR__ . '/fixtures/task_fixtures.php');
30 /**
31  * This file contains the unit tests for the task logging system.
32  *
33  * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
34  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  */
36 class core_task_logmanager extends advanced_testcase {
38     /**
39      * @var \moodle_database The original database prior to mocking
40      */
41     protected $DB;
43     /**
44      * Relevant tearDown for logging tests.
45      */
46     public function tearDown() {
47         global $DB;
49         // Ensure that any logging is always ended.
50         \core\task\logmanager::finalise_log();
52         if (null !== $this->DB) {
53             $DB = $this->DB;
54             $this->DB = null;
55         }
56     }
58     /**
59      * When the logmode is set to none, logging should not start.
60      */
61     public function test_logmode_none() {
62         global $CFG;
63         $this->resetAfterTest();
65         $CFG->task_logmode = \core\task\logmanager::MODE_NONE;
67         $initialbufferstate = ob_get_status();
69         $task = $this->get_test_adhoc_task();
70         \core\task\logmanager::start_logging($task);
72         // There will be no additional output buffer.
73         $this->assertEquals($initialbufferstate, ob_get_status());
74     }
76     /**
77      * When the logmode is set to all that log capture is started.
78      */
79     public function test_start_logmode_all() {
80         global $CFG;
81         $this->resetAfterTest();
83         $CFG->task_logmode = \core\task\logmanager::MODE_ALL;
85         $initialbufferstate = ob_get_status();
87         $task = $this->get_test_adhoc_task();
88         \core\task\logmanager::start_logging($task);
90         // Fetch the new output buffer state.
91         $state = ob_get_status();
93         // There will be no additional output buffer.
94         $this->assertNotEquals($initialbufferstate, $state);
95     }
97     /**
98      * When the logmode is set to fail that log capture is started.
99      */
100     public function test_start_logmode_fail() {
101         global $CFG;
102         $this->resetAfterTest();
104         $CFG->task_logmode = \core\task\logmanager::MODE_FAILONLY;
106         $initialbufferstate = ob_get_status();
108         $task = $this->get_test_adhoc_task();
109         \core\task\logmanager::start_logging($task);
111         // Fetch the new output buffer state.
112         $state = ob_get_status();
114         // There will be no additional output buffer.
115         $this->assertNotEquals($initialbufferstate, $state);
116     }
118     /**
119      * When the logmode is set to fail, passing adhoc tests should not be logged.
120      */
121     public function test_logmode_fail_with_passing_adhoc_task() {
122         global $CFG;
123         $this->resetAfterTest();
125         $CFG->task_logmode = \core\task\logmanager::MODE_FAILONLY;
127         $logger = $this->get_mocked_logger();
129         $initialbufferstate = ob_get_status();
131         $task = $this->get_test_adhoc_task();
132         \core\task\logmanager::start_logging($task);
134         \core\task\manager::adhoc_task_complete($task);
136         $this->assertEmpty($logger::$storelogfortask);
137     }
139     /**
140      * When the logmode is set to fail, passing scheduled tests should not be logged.
141      */
142     public function test_logmode_fail_with_passing_scheduled_task() {
143         global $CFG;
144         $this->resetAfterTest();
146         $CFG->task_logmode = \core\task\logmanager::MODE_FAILONLY;
148         $logger = $this->get_mocked_logger();
150         $initialbufferstate = ob_get_status();
152         $task = $this->get_test_scheduled_task();
153         \core\task\logmanager::start_logging($task);
155         \core\task\manager::scheduled_task_complete($task);
157         $this->assertEmpty($logger::$storelogfortask);
158     }
160     /**
161      * When the logmode is set to fail, failing adhoc tests should be logged.
162      */
163     public function test_logmode_fail_with_failing_adhoc_task() {
164         global $CFG;
166         $this->resetAfterTest();
168         // Mock the database. Marking jobs as failed updates a DB record which doesn't exist.
169         $this->mock_database();
171         $task = $this->get_test_adhoc_task();
173         $CFG->task_logmode = \core\task\logmanager::MODE_FAILONLY;
175         $logger = $this->get_mocked_logger();
177         \core\task\logmanager::start_logging($task);
178         \core\task\manager::adhoc_task_failed($task);
180         $this->assertCount(1, $logger::$storelogfortask);
181         $this->assertEquals($task, $logger::$storelogfortask[0][0]);
182         $this->assertTrue($logger::$storelogfortask[0][2]);
183     }
185     /**
186      * When the logmode is set to fail, failing scheduled tests should be logged.
187      */
188     public function test_logmode_fail_with_failing_scheduled_task() {
189         global $CFG;
191         $this->resetAfterTest();
193         // Mock the database. Marking jobs as failed updates a DB record which doesn't exist.
194         $this->mock_database();
196         $task = $this->get_test_scheduled_task();
198         $CFG->task_logmode = \core\task\logmanager::MODE_FAILONLY;
200         $logger = $this->get_mocked_logger();
202         \core\task\logmanager::start_logging($task);
203         \core\task\manager::scheduled_task_failed($task);
205         $this->assertCount(1, $logger::$storelogfortask);
206         $this->assertEquals($task, $logger::$storelogfortask[0][0]);
207         $this->assertTrue($logger::$storelogfortask[0][2]);
208     }
210     /**
211      * When the logmode is set to fail, failing adhoc tests should be logged.
212      */
213     public function test_logmode_any_with_failing_adhoc_task() {
214         global $CFG;
216         $this->resetAfterTest();
218         // Mock the database. Marking jobs as failed updates a DB record which doesn't exist.
219         $this->mock_database();
221         $task = $this->get_test_adhoc_task();
223         $CFG->task_logmode = \core\task\logmanager::MODE_FAILONLY;
225         $logger = $this->get_mocked_logger();
227         \core\task\logmanager::start_logging($task);
228         \core\task\manager::adhoc_task_failed($task);
230         $this->assertCount(1, $logger::$storelogfortask);
231         $this->assertEquals($task, $logger::$storelogfortask[0][0]);
232         $this->assertTrue($logger::$storelogfortask[0][2]);
233     }
235     /**
236      * When the logmode is set to fail, failing scheduled tests should be logged.
237      */
238     public function test_logmode_any_with_failing_scheduled_task() {
239         global $CFG;
241         $this->resetAfterTest();
243         // Mock the database. Marking jobs as failed updates a DB record which doesn't exist.
244         $this->mock_database();
246         $task = $this->get_test_scheduled_task();
248         $CFG->task_logmode = \core\task\logmanager::MODE_FAILONLY;
250         $logger = $this->get_mocked_logger();
252         \core\task\logmanager::start_logging($task);
253         \core\task\manager::scheduled_task_failed($task);
255         $this->assertCount(1, $logger::$storelogfortask);
256         $this->assertEquals($task, $logger::$storelogfortask[0][0]);
257         $this->assertTrue($logger::$storelogfortask[0][2]);
258     }
260     /**
261      * When the logmode is set to fail, passing adhoc tests should be logged.
262      */
263     public function test_logmode_any_with_passing_adhoc_task() {
264         global $CFG;
266         $this->resetAfterTest();
268         $this->mock_database();
270         $task = $this->get_test_adhoc_task();
272         $CFG->task_logmode = \core\task\logmanager::MODE_ALL;
274         $logger = $this->get_mocked_logger();
276         \core\task\logmanager::start_logging($task);
277         \core\task\manager::adhoc_task_complete($task);
279         $this->assertCount(1, $logger::$storelogfortask);
280         $this->assertEquals($task, $logger::$storelogfortask[0][0]);
281         $this->assertFalse($logger::$storelogfortask[0][2]);
282     }
284     /**
285      * When the logmode is set to fail, passing scheduled tests should be logged.
286      */
287     public function test_logmode_any_with_passing_scheduled_task() {
288         global $CFG;
290         $this->resetAfterTest();
292         $this->mock_database();
294         $task = $this->get_test_scheduled_task();
296         $CFG->task_logmode = \core\task\logmanager::MODE_ALL;
298         $logger = $this->get_mocked_logger();
300         \core\task\logmanager::start_logging($task);
301         \core\task\manager::scheduled_task_complete($task);
303         $this->assertCount(1, $logger::$storelogfortask);
304         $this->assertEquals($task, $logger::$storelogfortask[0][0]);
305         $this->assertFalse($logger::$storelogfortask[0][2]);
306     }
308     /**
309      * Ensure that start_logging cannot be called in a nested fashion.
310      */
311     public function test_prevent_nested_logging() {
312         $this->resetAfterTest();
314         $task = $this->get_test_adhoc_task();
315         \core\task\logmanager::start_logging($task);
317         $this->expectException(\coding_exception::class);
318         \core\task\logmanager::start_logging($task);
319     }
321     /**
322      * Ensure that logging can be called after a previous log has finished.
323      */
324     public function test_repeated_usages() {
325         $this->resetAfterTest();
327         $logger = $this->get_mocked_logger();
329         $task = $this->get_test_adhoc_task();
330         \core\task\logmanager::start_logging($task);
331         \core\task\logmanager::finalise_log();
333         \core\task\logmanager::start_logging($task);
334         \core\task\logmanager::finalise_log();
336         $this->assertCount(2, $logger::$storelogfortask);
337         $this->assertEquals($task, $logger::$storelogfortask[0][0]);
338         $this->assertFalse($logger::$storelogfortask[0][2]);
339         $this->assertEquals($task, $logger::$storelogfortask[1][0]);
340         $this->assertFalse($logger::$storelogfortask[1][2]);
341     }
343     /**
344      * Enusre that when finalise_log is called when logging is not active, nothing happens.
345      */
346     public function test_finalise_log_no_logging() {
347         $initialbufferstate = ob_get_status();
349         \core\task\logmanager::finalise_log();
351         // There will be no additional output buffer.
352         $this->assertEquals($initialbufferstate, ob_get_status());
353     }
355     /**
356      * When log capture is enabled, calls to the flush function should cause log output to be both returned and captured.
357      */
358     public function test_flush_on_own_buffer() {
359         $this->resetAfterTest();
361         $logger = $this->get_mocked_logger();
363         $testoutput = "I am the output under test.\n";
365         $task = $this->get_test_adhoc_task();
366         \core\task\logmanager::start_logging($task);
368         echo $testoutput;
370         $this->expectOutputString($testoutput);
371         \core\task\logmanager::flush();
373         // Finalise the log.
374         \core\task\logmanager::finalise_log();
376         $this->assertCount(1, $logger::$storelogfortask);
377         $this->assertEquals($testoutput, file_get_contents($logger::$storelogfortask[0][1]));
378     }
380     /**
381      * When log capture is enabled, calls to the flush function should not affect any subsequent ob_start.
382      */
383     public function test_flush_does_not_flush_inner_buffers() {
384         $this->resetAfterTest();
386         $logger = $this->get_mocked_logger();
388         $testoutput = "I am the output under test.\n";
390         $task = $this->get_test_adhoc_task();
391         \core\task\logmanager::start_logging($task);
393         ob_start();
394         echo $testoutput;
395         ob_end_clean();
397         \core\task\logmanager::flush();
399         // Finalise the log.
400         \core\task\logmanager::finalise_log();
402         $this->assertCount(1, $logger::$storelogfortask);
404         // The task logger should not have captured the content of the inner buffer.
405         $this->assertEquals('', file_get_contents($logger::$storelogfortask[0][1]));
406     }
408     /**
409      * When log capture is enabled, calls to the flush function should not affect any subsequent ob_start.
410      */
411     public function test_inner_flushed_buffers_are_logged() {
412         $this->resetAfterTest();
414         $logger = $this->get_mocked_logger();
416         $testoutput = "I am the output under test.\n";
418         $task = $this->get_test_adhoc_task();
419         \core\task\logmanager::start_logging($task);
421         // We are going to flush the inner buffer. That means that we should expect the output immediately.
422         $this->expectOutputString($testoutput);
424         ob_start();
425         echo $testoutput;
426         ob_end_flush();
428         // Finalise the log.
429         \core\task\logmanager::finalise_log();
431         $this->assertCount(1, $logger::$storelogfortask);
433         // The task logger should not have captured the content of the inner buffer.
434         $this->assertEquals($testoutput, file_get_contents($logger::$storelogfortask[0][1]));
435     }
437     /**
438      * Get an example adhoc task to use for testing.
439      *
440      * @return  \core\task\adhoc_task
441      */
442     protected function get_test_adhoc_task() : \core\task\adhoc_task {
443         $task = $this->getMockForAbstractClass(\core\task\adhoc_task::class);
445         // Mock a lock on the task.
446         $lock = $this->getMockBuilder(\core\lock\lock::class)
447             ->disableOriginalConstructor()
448             ->getMock();
449         $task->set_lock($lock);
451         return $task;
452     }
454     /**
455      * Get an example scheduled task to use for testing.
456      *
457      * @return  \core\task\scheduled_task
458      */
459     protected function get_test_scheduled_task() : \core\task\scheduled_task {
460         $task = $this->getMockForAbstractClass(\core\task\scheduled_task::class);
462         // Mock a lock on the task.
463         $lock = $this->getMockBuilder(\core\lock\lock::class)
464             ->disableOriginalConstructor()
465             ->getMock();
466         $task->set_lock($lock);
468         return $task;
469     }
471     /**
472      * Create and configure a mocked task logger.
473      *
474      * @return  \core\task\task_logger
475      */
476     protected function get_mocked_logger() {
477         global $CFG;
479         // We will modify config for the alternate logging class therefore we mnust reset after the test.
480         $this->resetAfterTest();
482         // Note PHPUnit does not support mocking static functions.
483         $CFG->task_log_class = \task_logging_test_mocked_logger::class;
484         \task_logging_test_mocked_logger::test_reset();
486         return $CFG->task_log_class;
487     }
489     /**
490      * Mock the database.
491      */
492     protected function mock_database() {
493         global $DB;
495         // Store the old Database for restoration in reset.
496         $this->DB = $DB;
498         $DB = $this->getMockBuilder(\moodle_database::class)
499             ->getMock();
501         $DB->method('get_record')
502             ->willReturn((object) []);
503     }
506 /**
507  * Mocked logger.
508  *
509  * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
510  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
511  */
512 class task_logging_test_mocked_logger implements \core\task\task_logger {
514     /**
515      * @var bool Whether this is configured.
516      */
517     public static $isconfigured = true;
519     /**
520      * @var array Arguments that store_log_for_task was called with.
521      */
522     public static $storelogfortask = [];
524     /**
525      * @var bool Whether this logger has a report.
526      */
527     public static $haslogreport = true;
529     /**
530      * Reset the test class.
531      */
532     public static function test_reset() {
533         self::$isconfigured = true;
534         self::$storelogfortask = [];
535         self::$haslogreport = true;
536     }
538     /**
539      * Whether the task is configured and ready to log.
540      *
541      * @return  bool
542      */
543     public static function is_configured() : bool {
544         return self::$isconfigured;
545     }
547     /**
548      * Store the log for the specified task.
549      *
550      * @param   \core\task\task_base   $task The task that the log belongs to.
551      * @param   string      $logpath The path to the log on disk
552      * @param   bool        $failed Whether the task failed
553      * @param   int         $dbreads The number of DB reads
554      * @param   int         $dbwrites The number of DB writes
555      * @param   float       $timestart The start time of the task
556      * @param   float       $timeend The end time of the task
557      */
558     public static function store_log_for_task(\core\task\task_base $task, string $logpath, bool $failed,
559             int $dbreads, int $dbwrites, float $timestart, float $timeend) {
560         self::$storelogfortask[] = func_get_args();
561     }
563     /**
564      * Whether this task logger has a report available.
565      *
566      * @return  bool
567      */
568     public static function has_log_report() : bool {
569         return self::$haslogreport;
570     }
572     /**
573      * Get any URL available for viewing relevant task log reports.
574      *
575      * @param   string      $classname The task class to fetch for
576      * @return  \moodle_url
577      */
578     public static function get_url_for_task_class(string $classname) : \moodle_url {
579         return new \moodle_url('');
580     }