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