MDL-62660 tool_dataprivacy: Add method for unit tests to run adhoc tasks
[moodle.git] / lib / phpunit / classes / advanced_testcase.php
blob546d11bee512f56b88bdf5890e0deb36d936f9f0
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.
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.
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
27 /**
28 * Advanced PHPUnit test case customised for Moodle.
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
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.
48 * Note: use setUp() or setUpBeforeClass() in your test cases.
50 * @param string $name
51 * @param array $data
52 * @param string $dataName
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->setRunTestInSeparateProcess(false);
62 /**
63 * Runs the bare test sequence.
64 * @return void
66 final public function runBare() {
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();
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);
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;
98 if (isset($e)) {
99 // cleanup after failed expectation
100 self::resetAllData();
101 throw $e;
104 if (!$this->testdbtransaction or $this->testdbtransaction->is_disposed()) {
105 $this->testdbtransaction = null;
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
114 self::resetAllData(null);
116 } else if ($this->resetAfterTest === false) {
117 if ($this->testdbtransaction) {
118 $this->testdbtransaction->allow_commit();
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());
132 self::resetAllData(true);
135 // make sure test did not forget to close transaction
136 if ($DB->is_transaction_started()) {
137 self::resetAllData();
138 if ($this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_PASSED
139 or $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED
140 or $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE) {
141 throw new coding_exception('Test '.$this->getName().' did not close database transaction');
147 * Creates a new FlatXmlDataSet with the given $xmlFile. (absolute path.)
149 * @param string $xmlFile
150 * @return PHPUnit\DbUnit\DataSet\FlatXmlDataSet
152 protected function createFlatXMLDataSet($xmlFile) {
153 return new PHPUnit\DbUnit\DataSet\FlatXmlDataSet($xmlFile);
157 * Creates a new XMLDataSet with the given $xmlFile. (absolute path.)
159 * @param string $xmlFile
160 * @return PHPUnit\DbUnit\DataSet\XmlDataSet
162 protected function createXMLDataSet($xmlFile) {
163 return new PHPUnit\DbUnit\DataSet\XmlDataSet($xmlFile);
167 * Creates a new CsvDataSet from the given array of csv files. (absolute paths.)
169 * @param array $files array tablename=>cvsfile
170 * @param string $delimiter
171 * @param string $enclosure
172 * @param string $escape
173 * @return PHPUnit\DbUnit\DataSet\CsvDataSet
175 protected function createCsvDataSet($files, $delimiter = ',', $enclosure = '"', $escape = '"') {
176 $dataSet = new PHPUnit\DbUnit\DataSet\CsvDataSet($delimiter, $enclosure, $escape);
177 foreach($files as $table=>$file) {
178 $dataSet->addTable($table, $file);
180 return $dataSet;
184 * Creates new ArrayDataSet from given array
186 * @param array $data array of tables, first row in each table is columns
187 * @return phpunit_ArrayDataSet
189 protected function createArrayDataSet(array $data) {
190 return new phpunit_ArrayDataSet($data);
194 * Load date into moodle database tables from standard PHPUnit data set.
196 * Note: it is usually better to use data generators
198 * @param PHPUnit\DbUnit\DataSet\IDataSet $dataset
199 * @return void
201 protected function loadDataSet(PHPUnit\DbUnit\DataSet\IDataSet $dataset) {
202 global $DB;
204 $structure = phpunit_util::get_tablestructure();
206 foreach($dataset->getTableNames() as $tablename) {
207 $table = $dataset->getTable($tablename);
208 $metadata = $dataset->getTableMetaData($tablename);
209 $columns = $metadata->getColumns();
211 $doimport = false;
212 if (isset($structure[$tablename]['id']) and $structure[$tablename]['id']->auto_increment) {
213 $doimport = in_array('id', $columns);
216 for($r=0; $r<$table->getRowCount(); $r++) {
217 $record = $table->getRow($r);
218 if ($doimport) {
219 $DB->import_record($tablename, $record);
220 } else {
221 $DB->insert_record($tablename, $record);
224 if ($doimport) {
225 $DB->get_manager()->reset_sequence(new xmldb_table($tablename));
231 * Call this method from test if you want to make sure that
232 * the resetting of database is done the slow way without transaction
233 * rollback.
235 * This is useful especially when testing stuff that is not compatible with transactions.
237 * @return void
239 public function preventResetByRollback() {
240 if ($this->testdbtransaction and !$this->testdbtransaction->is_disposed()) {
241 $this->testdbtransaction->allow_commit();
242 $this->testdbtransaction = null;
247 * Reset everything after current test.
248 * @param bool $reset true means reset state back, false means keep all data for the next test,
249 * null means reset state and show warnings if anything changed
250 * @return void
252 public function resetAfterTest($reset = true) {
253 $this->resetAfterTest = $reset;
257 * Return debugging messages from the current test.
258 * @return array with instances having 'message', 'level' and 'stacktrace' property.
260 public function getDebuggingMessages() {
261 return phpunit_util::get_debugging_messages();
265 * Clear all previous debugging messages in current test
266 * and revert to default DEVELOPER_DEBUG level.
268 public function resetDebugging() {
269 phpunit_util::reset_debugging();
273 * Assert that exactly debugging was just called once.
275 * Discards the debugging message if successful.
277 * @param null|string $debugmessage null means any
278 * @param null|string $debuglevel null means any
279 * @param string $message
281 public function assertDebuggingCalled($debugmessage = null, $debuglevel = null, $message = '') {
282 $debugging = $this->getDebuggingMessages();
283 $debugdisplaymessage = "\n".phpunit_util::display_debugging_messages(true);
284 $this->resetDebugging();
286 $count = count($debugging);
288 if ($count == 0) {
289 if ($message === '') {
290 $message = 'Expectation failed, debugging() not triggered.';
292 $this->fail($message);
294 if ($count > 1) {
295 if ($message === '') {
296 $message = 'Expectation failed, debugging() triggered '.$count.' times.'.$debugdisplaymessage;
298 $this->fail($message);
300 $this->assertEquals(1, $count);
302 $message .= $debugdisplaymessage;
303 $debug = reset($debugging);
304 if ($debugmessage !== null) {
305 $this->assertSame($debugmessage, $debug->message, $message);
307 if ($debuglevel !== null) {
308 $this->assertSame($debuglevel, $debug->level, $message);
313 * Asserts how many times debugging has been called.
315 * @param int $expectedcount The expected number of times
316 * @param array $debugmessages Expected debugging messages, one for each expected message.
317 * @param array $debuglevels Expected debugging levels, one for each expected message.
318 * @param string $message
319 * @return void
321 public function assertDebuggingCalledCount($expectedcount, $debugmessages = array(), $debuglevels = array(), $message = '') {
322 if (!is_int($expectedcount)) {
323 throw new coding_exception('assertDebuggingCalledCount $expectedcount argument should be an integer.');
326 $debugging = $this->getDebuggingMessages();
327 $message .= "\n".phpunit_util::display_debugging_messages(true);
328 $this->resetDebugging();
330 $this->assertEquals($expectedcount, count($debugging), $message);
332 if ($debugmessages) {
333 if (!is_array($debugmessages) || count($debugmessages) != $expectedcount) {
334 throw new coding_exception('assertDebuggingCalledCount $debugmessages should contain ' . $expectedcount . ' messages');
336 foreach ($debugmessages as $key => $debugmessage) {
337 $this->assertSame($debugmessage, $debugging[$key]->message, $message);
341 if ($debuglevels) {
342 if (!is_array($debuglevels) || count($debuglevels) != $expectedcount) {
343 throw new coding_exception('assertDebuggingCalledCount $debuglevels should contain ' . $expectedcount . ' messages');
345 foreach ($debuglevels as $key => $debuglevel) {
346 $this->assertSame($debuglevel, $debugging[$key]->level, $message);
352 * Call when no debugging() messages expected.
353 * @param string $message
355 public function assertDebuggingNotCalled($message = '') {
356 $debugging = $this->getDebuggingMessages();
357 $count = count($debugging);
359 if ($message === '') {
360 $message = 'Expectation failed, debugging() was triggered.';
362 $message .= "\n".phpunit_util::display_debugging_messages(true);
363 $this->resetDebugging();
364 $this->assertEquals(0, $count, $message);
368 * Assert that an event legacy data is equal to the expected value.
370 * @param mixed $expected expected data.
371 * @param \core\event\base $event the event object.
372 * @param string $message
373 * @return void
375 public function assertEventLegacyData($expected, \core\event\base $event, $message = '') {
376 $legacydata = phpunit_event_mock::testable_get_legacy_eventdata($event);
377 if ($message === '') {
378 $message = 'Event legacy data does not match expected value.';
380 $this->assertEquals($expected, $legacydata, $message);
384 * Assert that an event legacy log data is equal to the expected value.
386 * @param mixed $expected expected data.
387 * @param \core\event\base $event the event object.
388 * @param string $message
389 * @return void
391 public function assertEventLegacyLogData($expected, \core\event\base $event, $message = '') {
392 $legacydata = phpunit_event_mock::testable_get_legacy_logdata($event);
393 if ($message === '') {
394 $message = 'Event legacy log data does not match expected value.';
396 $this->assertEquals($expected, $legacydata, $message);
400 * Assert that an event is not using event->contxet.
401 * While restoring context might not be valid and it should not be used by event url
402 * or description methods.
404 * @param \core\event\base $event the event object.
405 * @param string $message
406 * @return void
408 public function assertEventContextNotUsed(\core\event\base $event, $message = '') {
409 // Save current event->context and set it to false.
410 $eventcontext = phpunit_event_mock::testable_get_event_context($event);
411 phpunit_event_mock::testable_set_event_context($event, false);
412 if ($message === '') {
413 $message = 'Event should not use context property of event in any method.';
416 // Test event methods should not use event->context.
417 $event->get_url();
418 $event->get_description();
419 $event->get_legacy_eventname();
420 phpunit_event_mock::testable_get_legacy_eventdata($event);
421 phpunit_event_mock::testable_get_legacy_logdata($event);
423 // Restore event->context.
424 phpunit_event_mock::testable_set_event_context($event, $eventcontext);
428 * Stores current time as the base for assertTimeCurrent().
430 * Note: this is called automatically before calling individual test methods.
431 * @return int current time
433 public function setCurrentTimeStart() {
434 $this->currenttimestart = time();
435 return $this->currenttimestart;
439 * Assert that: start < $time < time()
440 * @param int $time
441 * @param string $message
442 * @return void
444 public function assertTimeCurrent($time, $message = '') {
445 $msg = ($message === '') ? 'Time is lower that allowed start value' : $message;
446 $this->assertGreaterThanOrEqual($this->currenttimestart, $time, $msg);
447 $msg = ($message === '') ? 'Time is in the future' : $message;
448 $this->assertLessThanOrEqual(time(), $time, $msg);
452 * Starts message redirection.
454 * You can verify if messages were sent or not by inspecting the messages
455 * array in the returned messaging sink instance. The redirection
456 * can be stopped by calling $sink->close();
458 * @return phpunit_message_sink
460 public function redirectMessages() {
461 return phpunit_util::start_message_redirection();
465 * Starts email redirection.
467 * You can verify if email were sent or not by inspecting the email
468 * array in the returned phpmailer sink instance. The redirection
469 * can be stopped by calling $sink->close();
471 * @return phpunit_message_sink
473 public function redirectEmails() {
474 return phpunit_util::start_phpmailer_redirection();
478 * Starts event redirection.
480 * You can verify if events were triggered or not by inspecting the events
481 * array in the returned event sink instance. The redirection
482 * can be stopped by calling $sink->close();
484 * @return phpunit_event_sink
486 public function redirectEvents() {
487 return phpunit_util::start_event_redirection();
491 * Cleanup after all tests are executed.
493 * Note: do not forget to call this if overridden...
495 * @static
496 * @return void
498 public static function tearDownAfterClass() {
499 self::resetAllData();
504 * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
506 * @param bool $detectchanges
507 * true - changes in global state and database are reported as errors
508 * false - no errors reported
509 * null - only critical problems are reported as errors
510 * @return void
512 public static function resetAllData($detectchanges = false) {
513 phpunit_util::reset_all_data($detectchanges);
517 * Set current $USER, reset access cache.
518 * @static
519 * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
520 * @return void
522 public static function setUser($user = null) {
523 global $CFG, $DB;
525 if (is_object($user)) {
526 $user = clone($user);
527 } else if (!$user) {
528 $user = new stdClass();
529 $user->id = 0;
530 $user->mnethostid = $CFG->mnet_localhost_id;
531 } else {
532 $user = $DB->get_record('user', array('id'=>$user));
534 unset($user->description);
535 unset($user->access);
536 unset($user->preference);
538 // Enusre session is empty, as it may contain caches and user specific info.
539 \core\session\manager::init_empty_session();
541 \core\session\manager::set_user($user);
545 * Set current $USER to admin account, reset access cache.
546 * @static
547 * @return void
549 public static function setAdminUser() {
550 self::setUser(2);
554 * Set current $USER to guest account, reset access cache.
555 * @static
556 * @return void
558 public static function setGuestUser() {
559 self::setUser(1);
563 * Change server and default php timezones.
565 * @param string $servertimezone timezone to set in $CFG->timezone (not validated)
566 * @param string $defaultphptimezone timezone to fake default php timezone (must be valid)
568 public static function setTimezone($servertimezone = 'Australia/Perth', $defaultphptimezone = 'Australia/Perth') {
569 global $CFG;
570 $CFG->timezone = $servertimezone;
571 core_date::phpunit_override_default_php_timezone($defaultphptimezone);
572 core_date::set_default_server_timezone();
576 * Get data generator
577 * @static
578 * @return testing_data_generator
580 public static function getDataGenerator() {
581 return phpunit_util::get_data_generator();
585 * Returns UTL of the external test file.
587 * The result depends on the value of following constants:
588 * - TEST_EXTERNAL_FILES_HTTP_URL
589 * - TEST_EXTERNAL_FILES_HTTPS_URL
591 * They should point to standard external test files repository,
592 * it defaults to 'http://download.moodle.org/unittest'.
594 * False value means skip tests that require external files.
596 * @param string $path
597 * @param bool $https true if https required
598 * @return string url
600 public function getExternalTestFileUrl($path, $https = false) {
601 $path = ltrim($path, '/');
602 if ($path) {
603 $path = '/'.$path;
605 if ($https) {
606 if (defined('TEST_EXTERNAL_FILES_HTTPS_URL')) {
607 if (!TEST_EXTERNAL_FILES_HTTPS_URL) {
608 $this->markTestSkipped('Tests using external https test files are disabled');
610 return TEST_EXTERNAL_FILES_HTTPS_URL.$path;
612 return 'https://download.moodle.org/unittest'.$path;
615 if (defined('TEST_EXTERNAL_FILES_HTTP_URL')) {
616 if (!TEST_EXTERNAL_FILES_HTTP_URL) {
617 $this->markTestSkipped('Tests using external http test files are disabled');
619 return TEST_EXTERNAL_FILES_HTTP_URL.$path;
621 return 'http://download.moodle.org/unittest'.$path;
625 * Recursively visit all the files in the source tree. Calls the callback
626 * function with the pathname of each file found.
628 * @param string $path the folder to start searching from.
629 * @param string $callback the method of this class to call with the name of each file found.
630 * @param string $fileregexp a regexp used to filter the search (optional).
631 * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
632 * only files that match the regexp will be included. (default false).
633 * @param array $ignorefolders will not go into any of these folders (optional).
634 * @return void
636 public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
637 $files = scandir($path);
639 foreach ($files as $file) {
640 $filepath = $path .'/'. $file;
641 if (strpos($file, '.') === 0) {
642 /// Don't check hidden files.
643 continue;
644 } else if (is_dir($filepath)) {
645 if (!in_array($filepath, $ignorefolders)) {
646 $this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
648 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
649 $this->$callback($filepath);
655 * Wait for a second to roll over, ensures future calls to time() return a different result.
657 * This is implemented instead of sleep() as we do not need to wait a full second. In some cases
658 * due to calls we may wait more than sleep() would have, on average it will be less.
660 public function waitForSecond() {
661 $starttime = time();
662 while (time() == $starttime) {
663 usleep(50000);
668 * Run adhoc tasks, optionally matching the specified classname.
670 * @param string $matchclass The name of the class to match on.
671 * @param int $matchuserid The userid to match.
673 protected function runAdhocTasks($matchclass = '', $matchuserid = null) {
674 global $CFG, $DB;
675 require_once($CFG->libdir.'/cronlib.php');
677 $params = [];
678 if (!empty($matchclass)) {
679 if (strpos($matchclass, '\\') !== 0) {
680 $matchclass = '\\' . $matchclass;
682 $params['classname'] = $matchclass;
685 if (!empty($matchuserid)) {
686 $params['userid'] = $matchuserid;
689 $lock = $this->createMock(\core\lock\lock::class);
690 $cronlock = $this->createMock(\core\lock\lock::class);
692 $tasks = $DB->get_recordset('task_adhoc', $params);
693 foreach ($tasks as $record) {
694 // Note: This is for cron only.
695 // We do not lock the tasks.
696 $task = \core\task\manager::adhoc_task_from_record($record);
698 $user = null;
699 if ($userid = $task->get_userid()) {
700 // This task has a userid specified.
701 $user = \core_user::get_user($userid);
703 // User found. Check that they are suitable.
704 \core_user::require_active_user($user, true, true);
707 $task->set_lock($lock);
708 if (!$task->is_blocking()) {
709 $cronlock->release();
710 } else {
711 $task->set_cron_lock($cronlock);
714 cron_prepare_core_renderer();
715 $this->setUser($user);
717 $task->execute();
718 \core\task\manager::adhoc_task_complete($task);
720 unset($task);
722 $tasks->close();