MDL-67673 phpunit: Move tests to use new phpunit_dataset
[moodle.git] / lib / phpunit / classes / advanced_testcase.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  * Advanced test case.
19  *
20  * @package    core
21  * @category   phpunit
22  * @copyright  2012 Petr Skoda {@link http://skodak.org}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 /**
28  * Advanced PHPUnit test case customised for Moodle.
29  *
30  * @package    core
31  * @category   phpunit
32  * @copyright  2012 Petr Skoda {@link http://skodak.org}
33  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34  */
35 abstract class advanced_testcase extends base_testcase {
36     /** @var bool automatically reset everything? null means log changes */
37     private $resetAfterTest;
39     /** @var moodle_transaction */
40     private $testdbtransaction;
42     /** @var int timestamp used for current time asserts */
43     private $currenttimestart;
45     /**
46      * Constructs a test case with the given name.
47      *
48      * Note: use setUp() or setUpBeforeClass() in your test cases.
49      *
50      * @param string $name
51      * @param array  $data
52      * @param string $dataName
53      */
54     final public function __construct($name = null, array $data = array(), $dataName = '') {
55         parent::__construct($name, $data, $dataName);
57         $this->setBackupGlobals(false);
58         $this->setBackupStaticAttributes(false);
59         $this->setPreserveGlobalState(false);
60     }
62     /**
63      * Runs the bare test sequence.
64      * @return void
65      */
66     final public function runBare(): void {
67         global $DB;
69         if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) {
70             // this happens when previous test does not reset, we can not use transactions
71             $this->testdbtransaction = null;
73         } else if ($DB->get_dbfamily() === 'postgres' or $DB->get_dbfamily() === 'mssql') {
74             // database must allow rollback of DDL, so no mysql here
75             $this->testdbtransaction = $DB->start_delegated_transaction();
76         }
78         try {
79             $this->setCurrentTimeStart();
80             parent::runBare();
81             // set DB reference in case somebody mocked it in test
82             $DB = phpunit_util::get_global_backup('DB');
84             // Deal with any debugging messages.
85             $debugerror = phpunit_util::display_debugging_messages(true);
86             $this->resetDebugging();
87             if (!empty($debugerror)) {
88                 trigger_error('Unexpected debugging() call detected.'."\n".$debugerror, E_USER_NOTICE);
89             }
91         } catch (Exception $ex) {
92             $e = $ex;
93         } catch (Throwable $ex) {
94             // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
95             $e = $ex;
96         }
98         if (isset($e)) {
99             // cleanup after failed expectation
100             self::resetAllData();
101             throw $e;
102         }
104         if (!$this->testdbtransaction or $this->testdbtransaction->is_disposed()) {
105             $this->testdbtransaction = null;
106         }
108         if ($this->resetAfterTest === true) {
109             if ($this->testdbtransaction) {
110                 $DB->force_transaction_rollback();
111                 phpunit_util::reset_all_database_sequences();
112                 phpunit_util::$lastdbwrites = $DB->perf_get_writes(); // no db reset necessary
113             }
114             self::resetAllData(null);
116         } else if ($this->resetAfterTest === false) {
117             if ($this->testdbtransaction) {
118                 $this->testdbtransaction->allow_commit();
119             }
120             // keep all data untouched for other tests
122         } else {
123             // reset but log what changed
124             if ($this->testdbtransaction) {
125                 try {
126                     $this->testdbtransaction->allow_commit();
127                 } catch (dml_transaction_exception $e) {
128                     self::resetAllData();
129                     throw new coding_exception('Invalid transaction state detected in test '.$this->getName());
130                 }
131             }
132             self::resetAllData(true);
133         }
135         // Reset context cache.
136         context_helper::reset_caches();
138         // make sure test did not forget to close transaction
139         if ($DB->is_transaction_started()) {
140             self::resetAllData();
141             if ($this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_PASSED
142                 or $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED
143                 or $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE) {
144                 throw new coding_exception('Test '.$this->getName().' did not close database transaction');
145             }
146         }
147     }
149     /**
150      * Creates a new FlatXmlDataSet with the given $xmlFile. (absolute path.)
151      *
152      * @param string $xmlFile
153      * @return PHPUnit\DbUnit\DataSet\FlatXmlDataSet
154      */
155     protected function createFlatXMLDataSet($xmlFile) {
156         // TODO: MDL-67673 - removed
157         return new PHPUnit\DbUnit\DataSet\FlatXmlDataSet($xmlFile);
158     }
160     /**
161      * Creates a new XMLDataSet with the given $xmlFile. (absolute path.)
162      *
163      * @param string $xmlFile
164      * @return PHPUnit\DbUnit\DataSet\XmlDataSet
165      */
166     protected function createXMLDataSet($xmlFile) {
167         // TODO: MDL-67673 - deprecate this (debugging...)
168         return $this->dataset_from_files([$xmlFile]);
169     }
171     /**
172      * Creates a new CsvDataSet from the given array of csv files. (absolute paths.)
173      *
174      * @param array $files array tablename=>cvsfile
175      * @param string $delimiter unused
176      * @param string $enclosure unused
177      * @param string $escape unused
178      * @return phpunit_dataset
179      */
180     protected function createCsvDataSet($files, $delimiter = ',', $enclosure = '"', $escape = '"') {
181         // TODO: MDL-67673 - deprecate this (debugging...)
182         return $this->dataset_from_files($files);
183     }
185     /**
186      * Creates new ArrayDataSet from given array
187      *
188      * @param array $data array of tables, first row in each table is columns
189      * @return phpunit_ArrayDataSet
190      */
191     protected function createArrayDataSet(array $data) {
192         // TODO: MDL-67673 - deprecate this (debugging...)
193         return $this->dataset_from_array($data);
194     }
196     /**
197      * Load date into moodle database tables from standard PHPUnit data set.
198      *
199      * Note: it is usually better to use data generators
200      *
201      * @param phpunit_dataset $dataset
202      * @return void
203      */
204     protected function loadDataSet(phpunit_dataset $dataset) {
205         // TODO: MDL-67673 - deprecate this (debugging...)
206         $dataset->to_database();
207     }
209     /**
210      * Creates a new dataset from CVS/XML files.
211      *
212      * This method accepts an array of full paths to CSV or XML files to be loaded
213      * into the dataset. For CSV files, the name of the table which the file belongs
214      * to needs to be specified. Example:
215      *
216      *   $fullpaths = [
217      *       '/path/to/users.xml',
218      *       'course' => '/path/to/courses.csv',
219      *   ];
220      *
221      * @param array $files full paths to CSV or XML files to load.
222      * @return phpunit_dataset
223      */
224     protected function dataset_from_files(array $files) {
225         // We ignore $delimiter, $enclosure and $escape, use the default ones in your fixtures.
226         $dataset = new phpunit_dataset();
227         $dataset->from_files($files);
228         return $dataset;
229     }
231     /**
232      * Creates a new dataset from string (CSV or XML).
233      *
234      * @param string $content contents (CSV or XML) to load.
235      * @param string $type format of the content to be loaded (csv or xml).
236      * @param string $table name of the table which the file belongs to (only for CSV files).
237      */
238     protected function dataset_from_string(string $content, string $type, ?string $table = null) {
239         $dataset = new phpunit_dataset();
240         $dataset->from_string($content, $type, $table);
241         return $dataset;
242     }
244     /**
245      * Creates a new dataset from PHP array.
246      *
247      * @param array $data array of tables, see {@see phpunit_dataset::from_array()} for supported formats.
248      * @return phpunit_dataset
249      */
250     protected function dataset_from_array(array $data) {
251         $dataset = new phpunit_dataset();
252         $dataset->from_array($data);
253         return $dataset;
254     }
256     /**
257      * Call this method from test if you want to make sure that
258      * the resetting of database is done the slow way without transaction
259      * rollback.
260      *
261      * This is useful especially when testing stuff that is not compatible with transactions.
262      *
263      * @return void
264      */
265     public function preventResetByRollback() {
266         if ($this->testdbtransaction and !$this->testdbtransaction->is_disposed()) {
267             $this->testdbtransaction->allow_commit();
268             $this->testdbtransaction = null;
269         }
270     }
272     /**
273      * Reset everything after current test.
274      * @param bool $reset true means reset state back, false means keep all data for the next test,
275      *      null means reset state and show warnings if anything changed
276      * @return void
277      */
278     public function resetAfterTest($reset = true) {
279         $this->resetAfterTest = $reset;
280     }
282     /**
283      * Return debugging messages from the current test.
284      * @return array with instances having 'message', 'level' and 'stacktrace' property.
285      */
286     public function getDebuggingMessages() {
287         return phpunit_util::get_debugging_messages();
288     }
290     /**
291      * Clear all previous debugging messages in current test
292      * and revert to default DEVELOPER_DEBUG level.
293      */
294     public function resetDebugging() {
295         phpunit_util::reset_debugging();
296     }
298     /**
299      * Assert that exactly debugging was just called once.
300      *
301      * Discards the debugging message if successful.
302      *
303      * @param null|string $debugmessage null means any
304      * @param null|string $debuglevel null means any
305      * @param string $message
306      */
307     public function assertDebuggingCalled($debugmessage = null, $debuglevel = null, $message = '') {
308         $debugging = $this->getDebuggingMessages();
309         $debugdisplaymessage = "\n".phpunit_util::display_debugging_messages(true);
310         $this->resetDebugging();
312         $count = count($debugging);
314         if ($count == 0) {
315             if ($message === '') {
316                 $message = 'Expectation failed, debugging() not triggered.';
317             }
318             $this->fail($message);
319         }
320         if ($count > 1) {
321             if ($message === '') {
322                 $message = 'Expectation failed, debugging() triggered '.$count.' times.'.$debugdisplaymessage;
323             }
324             $this->fail($message);
325         }
326         $this->assertEquals(1, $count);
328         $message .= $debugdisplaymessage;
329         $debug = reset($debugging);
330         if ($debugmessage !== null) {
331             $this->assertSame($debugmessage, $debug->message, $message);
332         }
333         if ($debuglevel !== null) {
334             $this->assertSame($debuglevel, $debug->level, $message);
335         }
336     }
338     /**
339      * Asserts how many times debugging has been called.
340      *
341      * @param int $expectedcount The expected number of times
342      * @param array $debugmessages Expected debugging messages, one for each expected message.
343      * @param array $debuglevels Expected debugging levels, one for each expected message.
344      * @param string $message
345      * @return void
346      */
347     public function assertDebuggingCalledCount($expectedcount, $debugmessages = array(), $debuglevels = array(), $message = '') {
348         if (!is_int($expectedcount)) {
349             throw new coding_exception('assertDebuggingCalledCount $expectedcount argument should be an integer.');
350         }
352         $debugging = $this->getDebuggingMessages();
353         $message .= "\n".phpunit_util::display_debugging_messages(true);
354         $this->resetDebugging();
356         $this->assertEquals($expectedcount, count($debugging), $message);
358         if ($debugmessages) {
359             if (!is_array($debugmessages) || count($debugmessages) != $expectedcount) {
360                 throw new coding_exception('assertDebuggingCalledCount $debugmessages should contain ' . $expectedcount . ' messages');
361             }
362             foreach ($debugmessages as $key => $debugmessage) {
363                 $this->assertSame($debugmessage, $debugging[$key]->message, $message);
364             }
365         }
367         if ($debuglevels) {
368             if (!is_array($debuglevels) || count($debuglevels) != $expectedcount) {
369                 throw new coding_exception('assertDebuggingCalledCount $debuglevels should contain ' . $expectedcount . ' messages');
370             }
371             foreach ($debuglevels as $key => $debuglevel) {
372                 $this->assertSame($debuglevel, $debugging[$key]->level, $message);
373             }
374         }
375     }
377     /**
378      * Call when no debugging() messages expected.
379      * @param string $message
380      */
381     public function assertDebuggingNotCalled($message = '') {
382         $debugging = $this->getDebuggingMessages();
383         $count = count($debugging);
385         if ($message === '') {
386             $message = 'Expectation failed, debugging() was triggered.';
387         }
388         $message .= "\n".phpunit_util::display_debugging_messages(true);
389         $this->resetDebugging();
390         $this->assertEquals(0, $count, $message);
391     }
393     /**
394      * Assert that an event legacy data is equal to the expected value.
395      *
396      * @param mixed $expected expected data.
397      * @param \core\event\base $event the event object.
398      * @param string $message
399      * @return void
400      */
401     public function assertEventLegacyData($expected, \core\event\base $event, $message = '') {
402         $legacydata = phpunit_event_mock::testable_get_legacy_eventdata($event);
403         if ($message === '') {
404             $message = 'Event legacy data does not match expected value.';
405         }
406         $this->assertEquals($expected, $legacydata, $message);
407     }
409     /**
410      * Assert that an event legacy log data is equal to the expected value.
411      *
412      * @param mixed $expected expected data.
413      * @param \core\event\base $event the event object.
414      * @param string $message
415      * @return void
416      */
417     public function assertEventLegacyLogData($expected, \core\event\base $event, $message = '') {
418         $legacydata = phpunit_event_mock::testable_get_legacy_logdata($event);
419         if ($message === '') {
420             $message = 'Event legacy log data does not match expected value.';
421         }
422         $this->assertEquals($expected, $legacydata, $message);
423     }
425     /**
426      * Assert that an event is not using event->contxet.
427      * While restoring context might not be valid and it should not be used by event url
428      * or description methods.
429      *
430      * @param \core\event\base $event the event object.
431      * @param string $message
432      * @return void
433      */
434     public function assertEventContextNotUsed(\core\event\base $event, $message = '') {
435         // Save current event->context and set it to false.
436         $eventcontext = phpunit_event_mock::testable_get_event_context($event);
437         phpunit_event_mock::testable_set_event_context($event, false);
438         if ($message === '') {
439             $message = 'Event should not use context property of event in any method.';
440         }
442         // Test event methods should not use event->context.
443         $event->get_url();
444         $event->get_description();
445         $event->get_legacy_eventname();
446         phpunit_event_mock::testable_get_legacy_eventdata($event);
447         phpunit_event_mock::testable_get_legacy_logdata($event);
449         // Restore event->context.
450         phpunit_event_mock::testable_set_event_context($event, $eventcontext);
451     }
453     /**
454      * Stores current time as the base for assertTimeCurrent().
455      *
456      * Note: this is called automatically before calling individual test methods.
457      * @return int current time
458      */
459     public function setCurrentTimeStart() {
460         $this->currenttimestart = time();
461         return $this->currenttimestart;
462     }
464     /**
465      * Assert that: start < $time < time()
466      * @param int $time
467      * @param string $message
468      * @return void
469      */
470     public function assertTimeCurrent($time, $message = '') {
471         $msg =  ($message === '') ? 'Time is lower that allowed start value' : $message;
472         $this->assertGreaterThanOrEqual($this->currenttimestart, $time, $msg);
473         $msg =  ($message === '') ? 'Time is in the future' : $message;
474         $this->assertLessThanOrEqual(time(), $time, $msg);
475     }
477     /**
478      * Starts message redirection.
479      *
480      * You can verify if messages were sent or not by inspecting the messages
481      * array in the returned messaging sink instance. The redirection
482      * can be stopped by calling $sink->close();
483      *
484      * @return phpunit_message_sink
485      */
486     public function redirectMessages() {
487         return phpunit_util::start_message_redirection();
488     }
490     /**
491      * Starts email redirection.
492      *
493      * You can verify if email were sent or not by inspecting the email
494      * array in the returned phpmailer sink instance. The redirection
495      * can be stopped by calling $sink->close();
496      *
497      * @return phpunit_message_sink
498      */
499     public function redirectEmails() {
500         return phpunit_util::start_phpmailer_redirection();
501     }
503     /**
504      * Starts event redirection.
505      *
506      * You can verify if events were triggered or not by inspecting the events
507      * array in the returned event sink instance. The redirection
508      * can be stopped by calling $sink->close();
509      *
510      * @return phpunit_event_sink
511      */
512     public function redirectEvents() {
513         return phpunit_util::start_event_redirection();
514     }
516     /**
517      * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
518      *
519      * @param bool $detectchanges
520      *      true  - changes in global state and database are reported as errors
521      *      false - no errors reported
522      *      null  - only critical problems are reported as errors
523      * @return void
524      */
525     public static function resetAllData($detectchanges = false) {
526         phpunit_util::reset_all_data($detectchanges);
527     }
529     /**
530      * Set current $USER, reset access cache.
531      * @static
532      * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
533      * @return void
534      */
535     public static function setUser($user = null) {
536         global $CFG, $DB;
538         if (is_object($user)) {
539             $user = clone($user);
540         } else if (!$user) {
541             $user = new stdClass();
542             $user->id = 0;
543             $user->mnethostid = $CFG->mnet_localhost_id;
544         } else {
545             $user = $DB->get_record('user', array('id'=>$user));
546         }
547         unset($user->description);
548         unset($user->access);
549         unset($user->preference);
551         // Enusre session is empty, as it may contain caches and user specific info.
552         \core\session\manager::init_empty_session();
554         \core\session\manager::set_user($user);
555     }
557     /**
558      * Set current $USER to admin account, reset access cache.
559      * @static
560      * @return void
561      */
562     public static function setAdminUser() {
563         self::setUser(2);
564     }
566     /**
567      * Set current $USER to guest account, reset access cache.
568      * @static
569      * @return void
570      */
571     public static function setGuestUser() {
572         self::setUser(1);
573     }
575     /**
576      * Change server and default php timezones.
577      *
578      * @param string $servertimezone timezone to set in $CFG->timezone (not validated)
579      * @param string $defaultphptimezone timezone to fake default php timezone (must be valid)
580      */
581     public static function setTimezone($servertimezone = 'Australia/Perth', $defaultphptimezone = 'Australia/Perth') {
582         global $CFG;
583         $CFG->timezone = $servertimezone;
584         core_date::phpunit_override_default_php_timezone($defaultphptimezone);
585         core_date::set_default_server_timezone();
586     }
588     /**
589      * Get data generator
590      * @static
591      * @return testing_data_generator
592      */
593     public static function getDataGenerator() {
594         return phpunit_util::get_data_generator();
595     }
597     /**
598      * Returns UTL of the external test file.
599      *
600      * The result depends on the value of following constants:
601      *  - TEST_EXTERNAL_FILES_HTTP_URL
602      *  - TEST_EXTERNAL_FILES_HTTPS_URL
603      *
604      * They should point to standard external test files repository,
605      * it defaults to 'http://download.moodle.org/unittest'.
606      *
607      * False value means skip tests that require external files.
608      *
609      * @param string $path
610      * @param bool $https true if https required
611      * @return string url
612      */
613     public function getExternalTestFileUrl($path, $https = false) {
614         $path = ltrim($path, '/');
615         if ($path) {
616             $path = '/'.$path;
617         }
618         if ($https) {
619             if (defined('TEST_EXTERNAL_FILES_HTTPS_URL')) {
620                 if (!TEST_EXTERNAL_FILES_HTTPS_URL) {
621                     $this->markTestSkipped('Tests using external https test files are disabled');
622                 }
623                 return TEST_EXTERNAL_FILES_HTTPS_URL.$path;
624             }
625             return 'https://download.moodle.org/unittest'.$path;
626         }
628         if (defined('TEST_EXTERNAL_FILES_HTTP_URL')) {
629             if (!TEST_EXTERNAL_FILES_HTTP_URL) {
630                 $this->markTestSkipped('Tests using external http test files are disabled');
631             }
632             return TEST_EXTERNAL_FILES_HTTP_URL.$path;
633         }
634         return 'http://download.moodle.org/unittest'.$path;
635     }
637     /**
638      * Recursively visit all the files in the source tree. Calls the callback
639      * function with the pathname of each file found.
640      *
641      * @param string $path the folder to start searching from.
642      * @param string $callback the method of this class to call with the name of each file found.
643      * @param string $fileregexp a regexp used to filter the search (optional).
644      * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
645      *     only files that match the regexp will be included. (default false).
646      * @param array $ignorefolders will not go into any of these folders (optional).
647      * @return void
648      */
649     public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
650         $files = scandir($path);
652         foreach ($files as $file) {
653             $filepath = $path .'/'. $file;
654             if (strpos($file, '.') === 0) {
655                 /// Don't check hidden files.
656                 continue;
657             } else if (is_dir($filepath)) {
658                 if (!in_array($filepath, $ignorefolders)) {
659                     $this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
660                 }
661             } else if ($exclude xor preg_match($fileregexp, $filepath)) {
662                 $this->$callback($filepath);
663             }
664         }
665     }
667     /**
668      * Wait for a second to roll over, ensures future calls to time() return a different result.
669      *
670      * This is implemented instead of sleep() as we do not need to wait a full second. In some cases
671      * due to calls we may wait more than sleep() would have, on average it will be less.
672      */
673     public function waitForSecond() {
674         $starttime = time();
675         while (time() == $starttime) {
676             usleep(50000);
677         }
678     }
680     /**
681      * Run adhoc tasks, optionally matching the specified classname.
682      *
683      * @param   string  $matchclass The name of the class to match on.
684      * @param   int     $matchuserid The userid to match.
685      */
686     protected function runAdhocTasks($matchclass = '', $matchuserid = null) {
687         global $CFG, $DB;
688         require_once($CFG->libdir.'/cronlib.php');
690         $params = [];
691         if (!empty($matchclass)) {
692             if (strpos($matchclass, '\\') !== 0) {
693                 $matchclass = '\\' . $matchclass;
694             }
695             $params['classname'] = $matchclass;
696         }
698         if (!empty($matchuserid)) {
699             $params['userid'] = $matchuserid;
700         }
702         $lock = $this->createMock(\core\lock\lock::class);
703         $cronlock = $this->createMock(\core\lock\lock::class);
705         $tasks = $DB->get_recordset('task_adhoc', $params);
706         foreach ($tasks as $record) {
707             // Note: This is for cron only.
708             // We do not lock the tasks.
709             $task = \core\task\manager::adhoc_task_from_record($record);
711             $user = null;
712             if ($userid = $task->get_userid()) {
713                 // This task has a userid specified.
714                 $user = \core_user::get_user($userid);
716                 // User found. Check that they are suitable.
717                 \core_user::require_active_user($user, true, true);
718             }
720             $task->set_lock($lock);
721             if (!$task->is_blocking()) {
722                 $cronlock->release();
723             } else {
724                 $task->set_cron_lock($cronlock);
725             }
727             cron_prepare_core_renderer();
728             $this->setUser($user);
730             $task->execute();
731             \core\task\manager::adhoc_task_complete($task);
733             unset($task);
734         }
735         $tasks->close();
736     }