See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 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 * Utility class. 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 require_once (__DIR__.'/../../testing/classes/util.php'); 27 require_once (__DIR__ . "/coverage_info.php"); 28 29 /** 30 * Collection of utility methods. 31 * 32 * @package core 33 * @category phpunit 34 * @copyright 2012 Petr Skoda {@link http://skodak.org} 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 class phpunit_util extends testing_util { 38 /** 39 * @var int last value of db writes counter, used for db resetting 40 */ 41 public static $lastdbwrites = null; 42 43 /** @var array An array of original globals, restored after each test */ 44 protected static $globals = array(); 45 46 /** @var array list of debugging messages triggered during the last test execution */ 47 protected static $debuggings = array(); 48 49 /** @var phpunit_message_sink alternative target for moodle messaging */ 50 protected static $messagesink = null; 51 52 /** @var phpunit_phpmailer_sink alternative target for phpmailer messaging */ 53 protected static $phpmailersink = null; 54 55 /** @var phpunit_message_sink alternative target for moodle messaging */ 56 protected static $eventsink = null; 57 58 /** 59 * @var array Files to skip when resetting dataroot folder 60 */ 61 protected static $datarootskiponreset = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess'); 62 63 /** 64 * @var array Files to skip when dropping dataroot folder 65 */ 66 protected static $datarootskipondrop = array('.', '..', 'lock'); 67 68 /** 69 * Load global $CFG; 70 * @internal 71 * @static 72 * @return void 73 */ 74 public static function initialise_cfg() { 75 global $DB; 76 $dbhash = false; 77 try { 78 $dbhash = $DB->get_field('config', 'value', array('name'=>'phpunittest')); 79 } catch (Exception $e) { 80 // not installed yet 81 initialise_cfg(); 82 return; 83 } 84 if ($dbhash !== core_component::get_all_versions_hash()) { 85 // do not set CFG - the only way forward is to drop and reinstall 86 return; 87 } 88 // standard CFG init 89 initialise_cfg(); 90 } 91 92 /** 93 * Reset contents of all database tables to initial values, reset caches, etc. 94 * 95 * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! 96 * 97 * @static 98 * @param bool $detectchanges 99 * true - changes in global state and database are reported as errors 100 * false - no errors reported 101 * null - only critical problems are reported as errors 102 * @return void 103 */ 104 public static function reset_all_data($detectchanges = false) { 105 global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $FULLME, $FILTERLIB_PRIVATE; 106 107 // Stop any message redirection. 108 self::stop_message_redirection(); 109 110 // Stop any message redirection. 111 self::stop_event_redirection(); 112 113 // Start a new email redirection. 114 // This will clear any existing phpmailer redirection. 115 // We redirect all phpmailer output to this message sink which is 116 // called instead of phpmailer actually sending the message. 117 self::start_phpmailer_redirection(); 118 119 // We used to call gc_collect_cycles here to ensure desctructors were called between tests. 120 // This accounted for 25% of the total time running phpunit - so we removed it. 121 122 // Show any unhandled debugging messages, the runbare() could already reset it. 123 self::display_debugging_messages(); 124 self::reset_debugging(); 125 126 // reset global $DB in case somebody mocked it 127 $DB = self::get_global_backup('DB'); 128 129 if ($DB->is_transaction_started()) { 130 // we can not reset inside transaction 131 $DB->force_transaction_rollback(); 132 } 133 134 $resetdb = self::reset_database(); 135 $localename = self::get_locale_name(); 136 $warnings = array(); 137 138 if ($detectchanges === true) { 139 if ($resetdb) { 140 $warnings[] = 'Warning: unexpected database modification, resetting DB state'; 141 } 142 143 $oldcfg = self::get_global_backup('CFG'); 144 $oldsite = self::get_global_backup('SITE'); 145 foreach($CFG as $k=>$v) { 146 if (!property_exists($oldcfg, $k)) { 147 $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value'; 148 } else if ($oldcfg->$k !== $CFG->$k) { 149 $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value'; 150 } 151 unset($oldcfg->$k); 152 153 } 154 if ($oldcfg) { 155 foreach($oldcfg as $k=>$v) { 156 $warnings[] = 'Warning: unexpected removal of $CFG->'.$k; 157 } 158 } 159 160 if ($USER->id != 0) { 161 $warnings[] = 'Warning: unexpected change of $USER'; 162 } 163 164 if ($COURSE->id != $oldsite->id) { 165 $warnings[] = 'Warning: unexpected change of $COURSE'; 166 } 167 168 if ($FULLME !== self::get_global_backup('FULLME')) { 169 $warnings[] = 'Warning: unexpected change of $FULLME'; 170 } 171 172 if (setlocale(LC_TIME, 0) !== $localename) { 173 $warnings[] = 'Warning: unexpected change of locale'; 174 } 175 } 176 177 if (ini_get('max_execution_time') != 0) { 178 // This is special warning for all resets because we do not want any 179 // libraries to mess with timeouts unintentionally. 180 // Our PHPUnit integration is not supposed to change it either. 181 182 if ($detectchanges !== false) { 183 $warnings[] = 'Warning: max_execution_time was changed to '.ini_get('max_execution_time'); 184 } 185 set_time_limit(0); 186 } 187 188 // restore original globals 189 $_SERVER = self::get_global_backup('_SERVER'); 190 $CFG = self::get_global_backup('CFG'); 191 $SITE = self::get_global_backup('SITE'); 192 $FULLME = self::get_global_backup('FULLME'); 193 $_GET = array(); 194 $_POST = array(); 195 $_FILES = array(); 196 $_REQUEST = array(); 197 $COURSE = $SITE; 198 199 // reinitialise following globals 200 $OUTPUT = new bootstrap_renderer(); 201 $PAGE = new moodle_page(); 202 $FULLME = null; 203 $ME = null; 204 $SCRIPT = null; 205 $FILTERLIB_PRIVATE = null; 206 if (!empty($SESSION->notifications)) { 207 $SESSION->notifications = []; 208 } 209 210 // Empty sessison and set fresh new not-logged-in user. 211 \core\session\manager::init_empty_session(); 212 213 // reset all static caches 214 \core\event\manager::phpunit_reset(); 215 accesslib_clear_all_caches(true); 216 accesslib_reset_role_cache(); 217 get_string_manager()->reset_caches(true); 218 reset_text_filters_cache(true); 219 get_message_processors(false, true, true); 220 filter_manager::reset_caches(); 221 core_filetypes::reset_caches(); 222 \core_search\manager::clear_static(); 223 core_user::reset_caches(); 224 \core\output\icon_system::reset_caches(); 225 if (class_exists('core_media_manager', false)) { 226 core_media_manager::reset_caches(); 227 } 228 229 // Reset static unit test options. 230 if (class_exists('\availability_date\condition', false)) { 231 \availability_date\condition::set_current_time_for_test(0); 232 } 233 234 // Reset internal users. 235 core_user::reset_internal_users(); 236 237 // Clear static caches in calendar container. 238 if (class_exists('\core_calendar\local\event\container', false)) { 239 core_calendar\local\event\container::reset_caches(); 240 } 241 242 //TODO MDL-25290: add more resets here and probably refactor them to new core function 243 244 // Reset course and module caches. 245 core_courseformat\base::reset_course_cache(0); 246 get_fast_modinfo(0, 0, true); 247 248 // Reset other singletons. 249 if (class_exists('core_plugin_manager')) { 250 core_plugin_manager::reset_caches(true); 251 } 252 if (class_exists('\core\update\checker')) { 253 \core\update\checker::reset_caches(true); 254 } 255 if (class_exists('\core_course\customfield\course_handler')) { 256 \core_course\customfield\course_handler::reset_caches(); 257 } 258 if (class_exists('\core_reportbuilder\manager')) { 259 \core_reportbuilder\manager::reset_caches(); 260 } 261 262 // Clear static cache within restore. 263 if (class_exists('restore_section_structure_step')) { 264 restore_section_structure_step::reset_caches(); 265 } 266 267 // purge dataroot directory 268 self::reset_dataroot(); 269 270 // restore original config once more in case resetting of caches changed CFG 271 $CFG = self::get_global_backup('CFG'); 272 273 // inform data generator 274 self::get_data_generator()->reset(); 275 276 // fix PHP settings 277 error_reporting($CFG->debug); 278 279 // Reset the date/time class. 280 core_date::phpunit_reset(); 281 282 // Make sure the time locale is consistent - that is Australian English. 283 setlocale(LC_TIME, $localename); 284 285 // Reset the log manager cache. 286 get_log_manager(true); 287 288 // Reset user agent. 289 core_useragent::instance(true, null); 290 291 // verify db writes just in case something goes wrong in reset 292 if (self::$lastdbwrites != $DB->perf_get_writes()) { 293 error_log('Unexpected DB writes in phpunit_util::reset_all_data()'); 294 self::$lastdbwrites = $DB->perf_get_writes(); 295 } 296 297 if ($warnings) { 298 $warnings = implode("\n", $warnings); 299 trigger_error($warnings, E_USER_WARNING); 300 } 301 } 302 303 /** 304 * Reset all database tables to default values. 305 * @static 306 * @return bool true if reset done, false if skipped 307 */ 308 public static function reset_database() { 309 global $DB; 310 311 if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) { 312 return false; 313 } 314 315 if (!parent::reset_database()) { 316 return false; 317 } 318 319 self::$lastdbwrites = $DB->perf_get_writes(); 320 321 return true; 322 } 323 324 /** 325 * Called during bootstrap only! 326 * @internal 327 * @static 328 * @return void 329 */ 330 public static function bootstrap_init() { 331 global $CFG, $SITE, $DB, $FULLME; 332 333 // backup the globals 334 self::$globals['_SERVER'] = $_SERVER; 335 self::$globals['CFG'] = clone($CFG); 336 self::$globals['SITE'] = clone($SITE); 337 self::$globals['DB'] = $DB; 338 self::$globals['FULLME'] = $FULLME; 339 340 // refresh data in all tables, clear caches, etc. 341 self::reset_all_data(); 342 } 343 344 /** 345 * Print some Moodle related info to console. 346 * @internal 347 * @static 348 * @return void 349 */ 350 public static function bootstrap_moodle_info() { 351 echo self::get_site_info(); 352 } 353 354 /** 355 * Returns original state of global variable. 356 * @static 357 * @param string $name 358 * @return mixed 359 */ 360 public static function get_global_backup($name) { 361 if ($name === 'DB') { 362 // no cloning of database object, 363 // we just need the original reference, not original state 364 return self::$globals['DB']; 365 } 366 if (isset(self::$globals[$name])) { 367 if (is_object(self::$globals[$name])) { 368 $return = clone(self::$globals[$name]); 369 return $return; 370 } else { 371 return self::$globals[$name]; 372 } 373 } 374 return null; 375 } 376 377 /** 378 * Is this site initialised to run unit tests? 379 * 380 * @static 381 * @return int array errorcode=>message, 0 means ok 382 */ 383 public static function testing_ready_problem() { 384 global $DB; 385 386 $localename = self::get_locale_name(); 387 if (setlocale(LC_TIME, $localename) === false) { 388 return array(PHPUNIT_EXITCODE_CONFIGERROR, "Required locale '$localename' is not installed."); 389 } 390 391 if (!self::is_test_site()) { 392 // dataroot was verified in bootstrap, so it must be DB 393 return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix'); 394 } 395 396 $tables = $DB->get_tables(false); 397 if (empty($tables)) { 398 return array(PHPUNIT_EXITCODE_INSTALL, ''); 399 } 400 401 if (!self::is_test_data_updated()) { 402 return array(PHPUNIT_EXITCODE_REINSTALL, ''); 403 } 404 405 return array(0, ''); 406 } 407 408 /** 409 * Drop all test site data. 410 * 411 * Note: To be used from CLI scripts only. 412 * 413 * @static 414 * @param bool $displayprogress if true, this method will echo progress information. 415 * @return void may terminate execution with exit code 416 */ 417 public static function drop_site($displayprogress = false) { 418 global $DB, $CFG; 419 420 if (!self::is_test_site()) { 421 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!'); 422 } 423 424 // Purge dataroot 425 if ($displayprogress) { 426 echo "Purging dataroot:\n"; 427 } 428 429 self::reset_dataroot(); 430 testing_initdataroot($CFG->dataroot, 'phpunit'); 431 432 // Drop all tables. 433 self::drop_database($displayprogress); 434 435 // Drop dataroot. 436 self::drop_dataroot(); 437 } 438 439 /** 440 * Perform a fresh test site installation 441 * 442 * Note: To be used from CLI scripts only. 443 * 444 * @static 445 * @return void may terminate execution with exit code 446 */ 447 public static function install_site() { 448 global $DB, $CFG; 449 450 if (!self::is_test_site()) { 451 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!'); 452 } 453 454 if ($DB->get_tables()) { 455 list($errorcode, $message) = self::testing_ready_problem(); 456 if ($errorcode) { 457 phpunit_bootstrap_error(PHPUNIT_EXITCODE_REINSTALL, 'Database tables already present, Moodle PHPUnit test environment can not be initialised'); 458 } else { 459 phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised'); 460 } 461 } 462 463 $options = array(); 464 $options['adminpass'] = 'admin'; 465 $options['shortname'] = 'phpunit'; 466 $options['fullname'] = 'PHPUnit test site'; 467 468 install_cli_database($options, false); 469 470 // Set the admin email address. 471 $DB->set_field('user', 'email', 'admin@example.com', array('username' => 'admin')); 472 473 // Disable all logging for performance and sanity reasons. 474 set_config('enabled_stores', '', 'tool_log'); 475 476 // Remove any default blocked hosts and port restrictions, to avoid blocking tests (eg those using local files). 477 set_config('curlsecurityblockedhosts', ''); 478 set_config('curlsecurityallowedport', ''); 479 480 // Execute all the adhoc tasks. 481 while ($task = \core\task\manager::get_next_adhoc_task(time())) { 482 $task->execute(); 483 \core\task\manager::adhoc_task_complete($task); 484 } 485 486 // We need to keep the installed dataroot filedir files. 487 // So each time we reset the dataroot before running a test, the default files are still installed. 488 self::save_original_data_files(); 489 490 // Store version hash in the database and in a file. 491 self::store_versions_hash(); 492 493 // Store database data and structure. 494 self::store_database_state(); 495 } 496 497 /** 498 * Builds dirroot/phpunit.xml file using defaults from /phpunit.xml.dist 499 * @static 500 * @return bool true means main config file created, false means only dataroot file created 501 */ 502 public static function build_config_file() { 503 global $CFG; 504 505 $template = <<<EOF 506 <testsuite name="@component@_testsuite"> 507 <directory suffix="_test.php">@dir@</directory> 508 </testsuite> 509 510 EOF; 511 $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); 512 513 $suites = ''; 514 $includelists = []; 515 $excludelists = []; 516 517 $subsystems = core_component::get_core_subsystems(); 518 $subsystems['core'] = $CFG->dirroot . '/lib'; 519 foreach ($subsystems as $subsystem => $fulldir) { 520 if (empty($fulldir)) { 521 continue; 522 } 523 if (!file_exists("{$fulldir}/tests/")) { 524 // There are no tests - skip this directory. 525 continue; 526 } 527 528 $dir = substr($fulldir, strlen($CFG->dirroot) + 1); 529 if ($coverageinfo = self::get_coverage_info($fulldir)) { 530 $includelists = array_merge($includelists, $coverageinfo->get_includelists($dir)); 531 $excludelists = array_merge($excludelists, $coverageinfo->get_excludelists($dir)); 532 } 533 } 534 535 $plugintypes = core_component::get_plugin_types(); 536 ksort($plugintypes); 537 foreach (array_keys($plugintypes) as $type) { 538 $plugs = core_component::get_plugin_list($type); 539 ksort($plugs); 540 foreach ($plugs as $plug => $plugindir) { 541 if (!file_exists("{$plugindir}/tests/")) { 542 // There are no tests - skip this directory. 543 continue; 544 } 545 546 $dir = substr($plugindir, strlen($CFG->dirroot) + 1); 547 $testdir = "{$dir}/tests"; 548 $component = "{$type}_{$plug}"; 549 550 $suite = str_replace('@component@', $component, $template); 551 $suite = str_replace('@dir@', $testdir, $suite); 552 553 $suites .= $suite; 554 555 if ($coverageinfo = self::get_coverage_info($plugindir)) { 556 557 $includelists = array_merge($includelists, $coverageinfo->get_includelists($dir)); 558 $excludelists = array_merge($excludelists, $coverageinfo->get_excludelists($dir)); 559 } 560 } 561 } 562 563 // Start a sequence between 100000 and 199000 to ensure each call to init produces 564 // different ids in the database. This reduces the risk that hard coded values will 565 // end up being placed in phpunit or behat test code. 566 $sequencestart = 100000 + mt_rand(0, 99) * 1000; 567 568 $data = preg_replace('| *<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', trim($suites, "\n"), $data, 1); 569 $data = str_replace( 570 '<const name="PHPUNIT_SEQUENCE_START" value=""/>', 571 '<const name="PHPUNIT_SEQUENCE_START" value="' . $sequencestart . '"/>', 572 $data); 573 574 $coverages = self::get_coverage_config($includelists, $excludelists); 575 $data = preg_replace('| *<!--@coveragelist@-->|s', trim($coverages, "\n"), $data); 576 577 $result = false; 578 if (is_writable($CFG->dirroot)) { 579 if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) { 580 testing_fix_file_permissions("$CFG->dirroot/phpunit.xml"); 581 } 582 } 583 584 return (bool)$result; 585 } 586 587 /** 588 * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist 589 * 590 * @static 591 * @return void, stops if can not write files 592 */ 593 public static function build_component_config_files() { 594 global $CFG; 595 596 $template = <<<EOT 597 <testsuites> 598 <testsuite name="@component@_testsuite"> 599 <directory suffix="_test.php">.</directory> 600 </testsuite> 601 </testsuites> 602 EOT; 603 $coveragedefault = <<<EOT 604 <include> 605 <directory suffix=".php">.</directory> 606 </include> 607 <exclude> 608 <directory suffix="_test.php">.</directory> 609 </exclude> 610 EOT; 611 612 // Start a sequence between 100000 and 199000 to ensure each call to init produces 613 // different ids in the database. This reduces the risk that hard coded values will 614 // end up being placed in phpunit or behat test code. 615 $sequencestart = 100000 + mt_rand(0, 99) * 1000; 616 617 // Use the upstream file as source for the distributed configurations 618 $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); 619 $ftemplate = preg_replace('| *<!--All core suites.*</testsuites>|s', '<!--@component_suite@-->', $ftemplate); 620 621 // Gets all the components with tests 622 $components = tests_finder::get_components_with_tests('phpunit'); 623 624 // Create the corresponding phpunit.xml file for each component 625 foreach ($components as $cname => $cpath) { 626 // Calculate the component suite 627 $ctemplate = $template; 628 $ctemplate = str_replace('@component@', $cname, $ctemplate); 629 630 $fcontents = str_replace('<!--@component_suite@-->', $ctemplate, $ftemplate); 631 632 // Check for coverage configurations. 633 if ($coverageinfo = self::get_coverage_info($cpath)) { 634 $coverages = self::get_coverage_config($coverageinfo->get_includelists(''), $coverageinfo->get_excludelists('')); 635 } else { 636 $coverages = $coveragedefault; 637 } 638 $fcontents = preg_replace('| *<!--@coveragelist@-->|s', trim($coverages, "\n"), $fcontents); 639 640 // Apply it to the file template. 641 $fcontents = str_replace( 642 '<const name="PHPUNIT_SEQUENCE_START" value=""/>', 643 '<const name="PHPUNIT_SEQUENCE_START" value="' . $sequencestart . '"/>', 644 $fcontents); 645 646 // fix link to schema 647 $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/'); 648 $fcontents = str_replace('lib/phpunit/', str_repeat('../', $level).'lib/phpunit/', $fcontents); 649 650 // Write the file 651 $result = false; 652 if (is_writable($cpath)) { 653 if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) { 654 testing_fix_file_permissions("$cpath/phpunit.xml"); 655 } 656 } 657 // Problems writing file, throw error 658 if (!$result) { 659 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGWARNING, "Can not create $cpath/phpunit.xml configuration file, verify dir permissions"); 660 } 661 } 662 } 663 664 /** 665 * To be called from debugging() only. 666 * @param string $message 667 * @param int $level 668 * @param string $from 669 */ 670 public static function debugging_triggered($message, $level, $from) { 671 // Store only if debugging triggered from actual test, 672 // we need normal debugging outside of tests to find problems in our phpunit integration. 673 $backtrace = debug_backtrace(); 674 675 foreach ($backtrace as $bt) { 676 if (isset($bt['object']) and is_object($bt['object']) 677 && $bt['object'] instanceof PHPUnit\Framework\TestCase) { 678 $debug = new stdClass(); 679 $debug->message = $message; 680 $debug->level = $level; 681 $debug->from = $from; 682 683 self::$debuggings[] = $debug; 684 685 return true; 686 } 687 } 688 return false; 689 } 690 691 /** 692 * Resets the list of debugging messages. 693 */ 694 public static function reset_debugging() { 695 self::$debuggings = array(); 696 set_debugging(DEBUG_DEVELOPER); 697 } 698 699 /** 700 * Returns all debugging messages triggered during test. 701 * @return array with instances having message, level and stacktrace property. 702 */ 703 public static function get_debugging_messages() { 704 return self::$debuggings; 705 } 706 707 /** 708 * Prints out any debug messages accumulated during test execution. 709 * 710 * @param bool $return true to return the messages or false to print them directly. Default false. 711 * @return bool|string false if no debug messages, true if debug triggered or string of messages 712 */ 713 public static function display_debugging_messages($return = false) { 714 if (empty(self::$debuggings)) { 715 return false; 716 } 717 718 $debugstring = ''; 719 foreach(self::$debuggings as $debug) { 720 $debugstring .= 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n"; 721 } 722 723 if ($return) { 724 return $debugstring; 725 } 726 echo $debugstring; 727 return true; 728 } 729 730 /** 731 * Start message redirection. 732 * 733 * Note: Do not call directly from tests, 734 * use $sink = $this->redirectMessages() instead. 735 * 736 * @return phpunit_message_sink 737 */ 738 public static function start_message_redirection() { 739 if (self::$messagesink) { 740 self::stop_message_redirection(); 741 } 742 self::$messagesink = new phpunit_message_sink(); 743 return self::$messagesink; 744 } 745 746 /** 747 * End message redirection. 748 * 749 * Note: Do not call directly from tests, 750 * use $sink->close() instead. 751 */ 752 public static function stop_message_redirection() { 753 self::$messagesink = null; 754 } 755 756 /** 757 * Are messages redirected to some sink? 758 * 759 * Note: to be called from messagelib.php only! 760 * 761 * @return bool 762 */ 763 public static function is_redirecting_messages() { 764 return !empty(self::$messagesink); 765 } 766 767 /** 768 * To be called from messagelib.php only! 769 * 770 * @param stdClass $message record from messages table 771 * @return bool true means send message, false means message "sent" to sink. 772 */ 773 public static function message_sent($message) { 774 if (self::$messagesink) { 775 self::$messagesink->add_message($message); 776 } 777 } 778 779 /** 780 * Start phpmailer redirection. 781 * 782 * Note: Do not call directly from tests, 783 * use $sink = $this->redirectEmails() instead. 784 * 785 * @return phpunit_phpmailer_sink 786 */ 787 public static function start_phpmailer_redirection() { 788 if (self::$phpmailersink) { 789 // If an existing mailer sink is active, just clear it. 790 self::$phpmailersink->clear(); 791 } else { 792 self::$phpmailersink = new phpunit_phpmailer_sink(); 793 } 794 return self::$phpmailersink; 795 } 796 797 /** 798 * End phpmailer redirection. 799 * 800 * Note: Do not call directly from tests, 801 * use $sink->close() instead. 802 */ 803 public static function stop_phpmailer_redirection() { 804 self::$phpmailersink = null; 805 } 806 807 /** 808 * Are messages for phpmailer redirected to some sink? 809 * 810 * Note: to be called from moodle_phpmailer.php only! 811 * 812 * @return bool 813 */ 814 public static function is_redirecting_phpmailer() { 815 return !empty(self::$phpmailersink); 816 } 817 818 /** 819 * To be called from messagelib.php only! 820 * 821 * @param stdClass $message record from messages table 822 * @return bool true means send message, false means message "sent" to sink. 823 */ 824 public static function phpmailer_sent($message) { 825 if (self::$phpmailersink) { 826 self::$phpmailersink->add_message($message); 827 } 828 } 829 830 /** 831 * Start event redirection. 832 * 833 * @private 834 * Note: Do not call directly from tests, 835 * use $sink = $this->redirectEvents() instead. 836 * 837 * @return phpunit_event_sink 838 */ 839 public static function start_event_redirection() { 840 if (self::$eventsink) { 841 self::stop_event_redirection(); 842 } 843 self::$eventsink = new phpunit_event_sink(); 844 return self::$eventsink; 845 } 846 847 /** 848 * End event redirection. 849 * 850 * @private 851 * Note: Do not call directly from tests, 852 * use $sink->close() instead. 853 */ 854 public static function stop_event_redirection() { 855 self::$eventsink = null; 856 } 857 858 /** 859 * Are events redirected to some sink? 860 * 861 * Note: to be called from \core\event\base only! 862 * 863 * @private 864 * @return bool 865 */ 866 public static function is_redirecting_events() { 867 return !empty(self::$eventsink); 868 } 869 870 /** 871 * To be called from \core\event\base only! 872 * 873 * @private 874 * @param \core\event\base $event record from event_read table 875 * @return bool true means send event, false means event "sent" to sink. 876 */ 877 public static function event_triggered(\core\event\base $event) { 878 if (self::$eventsink) { 879 self::$eventsink->add_event($event); 880 } 881 } 882 883 /** 884 * Gets the name of the locale for testing environment (Australian English) 885 * depending on platform environment. 886 * 887 * @return string the locale name. 888 */ 889 protected static function get_locale_name() { 890 global $CFG; 891 if ($CFG->ostype === 'WINDOWS') { 892 return 'English_Australia.1252'; 893 } else { 894 return 'en_AU.UTF-8'; 895 } 896 } 897 898 /** 899 * Executes all adhoc tasks in the queue. Useful for testing asynchronous behaviour. 900 * 901 * @return void 902 */ 903 public static function run_all_adhoc_tasks() { 904 $now = time(); 905 while (($task = \core\task\manager::get_next_adhoc_task($now)) !== null) { 906 try { 907 $task->execute(); 908 \core\task\manager::adhoc_task_complete($task); 909 } catch (Exception $e) { 910 \core\task\manager::adhoc_task_failed($task); 911 } 912 } 913 } 914 915 /** 916 * Helper function to call a protected/private method of an object using reflection. 917 * 918 * Example 1. Calling a protected object method: 919 * $result = call_internal_method($myobject, 'method_name', [$param1, $param2], '\my\namespace\myobjectclassname'); 920 * 921 * Example 2. Calling a protected static method: 922 * $result = call_internal_method(null, 'method_name', [$param1, $param2], '\my\namespace\myclassname'); 923 * 924 * @param object|null $object the object on which to call the method, or null if calling a static method. 925 * @param string $methodname the name of the protected/private method. 926 * @param array $params the array of function params to pass to the method. 927 * @param string $classname the fully namespaced name of the class the object was created from (base in the case of mocks), 928 * or the name of the static class when calling a static method. 929 * @return mixed the respective return value of the method. 930 */ 931 public static function call_internal_method($object, $methodname, array $params, $classname) { 932 $reflection = new \ReflectionClass($classname); 933 $method = $reflection->getMethod($methodname); 934 $method->setAccessible(true); 935 return $method->invokeArgs($object, $params); 936 } 937 938 /** 939 * Pad the supplied string with $level levels of indentation. 940 * 941 * @param string $string The string to pad 942 * @param int $level The number of levels of indentation to pad 943 * @return string 944 */ 945 protected static function pad(string $string, int $level) : string { 946 return str_repeat(" ", $level * 2) . "{$string}\n"; 947 } 948 949 /** 950 * Get the coverage config for the supplied includelist and excludelist configuration. 951 * 952 * @param array[] $includelists The list of files/folders in the includelist. 953 * @param array[] $excludelists The list of files/folders in the excludelist. 954 * @return string 955 */ 956 protected static function get_coverage_config(array $includelists, array $excludelists) : string { 957 $coverages = ''; 958 if (!empty($includelists)) { 959 $coverages .= self::pad("<include>", 2); 960 foreach ($includelists as $line) { 961 $coverages .= self::pad($line, 3); 962 } 963 $coverages .= self::pad("</include>", 2); 964 if (!empty($excludelists)) { 965 $coverages .= self::pad("<exclude>", 2); 966 foreach ($excludelists as $line) { 967 $coverages .= self::pad($line, 3); 968 } 969 $coverages .= self::pad("</exclude>", 2); 970 } 971 } 972 973 return $coverages; 974 } 975 976 /** 977 * Get the phpunit_coverage_info for the specified plugin or subsystem directory. 978 * 979 * @param string $fulldir The directory to find the coverage info file in. 980 * @return phpunit_coverage_info 981 */ 982 protected static function get_coverage_info(string $fulldir): phpunit_coverage_info { 983 $coverageconfig = "{$fulldir}/tests/coverage.php"; 984 if (file_exists($coverageconfig)) { 985 $coverageinfo = require($coverageconfig); 986 if (!$coverageinfo instanceof phpunit_coverage_info) { 987 throw new \coding_exception("{$coverageconfig} does not return a phpunit_coverage_info"); 988 } 989 990 return $coverageinfo; 991 } 992 993 return new phpunit_coverage_info();; 994 } 995 996 /** 997 * Whether the current process is an isolated test process. 998 * 999 * @return bool 1000 */ 1001 public static function is_in_isolated_process(): bool { 1002 // Note: There is no function to call, or much to go by in order to tell whether we are in an isolated process 1003 // during Bootstrap, when this function is called. 1004 // We can do so by testing the existence of the wrapper function, but there is nothing set until that point. 1005 return function_exists('__phpunit_run_isolated_test'); 1006 } 1007 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body