Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 core_text::reset_caches(); 220 get_message_processors(false, true, true); 221 filter_manager::reset_caches(); 222 core_filetypes::reset_caches(); 223 \core_search\manager::clear_static(); 224 core_user::reset_caches(); 225 \core\output\icon_system::reset_caches(); 226 if (class_exists('core_media_manager', false)) { 227 core_media_manager::reset_caches(); 228 } 229 230 // Reset static unit test options. 231 if (class_exists('\availability_date\condition', false)) { 232 \availability_date\condition::set_current_time_for_test(0); 233 } 234 235 // Reset internal users. 236 core_user::reset_internal_users(); 237 238 // Clear static caches in calendar container. 239 if (class_exists('\core_calendar\local\event\container', false)) { 240 core_calendar\local\event\container::reset_caches(); 241 } 242 243 //TODO MDL-25290: add more resets here and probably refactor them to new core function 244 245 // Reset course and module caches. 246 if (class_exists('format_base')) { 247 // If file containing class is not loaded, there is no cache there anyway. 248 format_base::reset_course_cache(0); 249 } 250 get_fast_modinfo(0, 0, true); 251 252 // Reset other singletons. 253 if (class_exists('core_plugin_manager')) { 254 core_plugin_manager::reset_caches(true); 255 } 256 if (class_exists('\core\update\checker')) { 257 \core\update\checker::reset_caches(true); 258 } 259 if (class_exists('\core_course\customfield\course_handler')) { 260 \core_course\customfield\course_handler::reset_caches(); 261 } 262 263 // Clear static cache within restore. 264 if (class_exists('restore_section_structure_step')) { 265 restore_section_structure_step::reset_caches(); 266 } 267 268 // purge dataroot directory 269 self::reset_dataroot(); 270 271 // restore original config once more in case resetting of caches changed CFG 272 $CFG = self::get_global_backup('CFG'); 273 274 // inform data generator 275 self::get_data_generator()->reset(); 276 277 // fix PHP settings 278 error_reporting($CFG->debug); 279 280 // Reset the date/time class. 281 core_date::phpunit_reset(); 282 283 // Make sure the time locale is consistent - that is Australian English. 284 setlocale(LC_TIME, $localename); 285 286 // Reset the log manager cache. 287 get_log_manager(true); 288 289 // Reset user agent. 290 core_useragent::instance(true, null); 291 292 // verify db writes just in case something goes wrong in reset 293 if (self::$lastdbwrites != $DB->perf_get_writes()) { 294 error_log('Unexpected DB writes in phpunit_util::reset_all_data()'); 295 self::$lastdbwrites = $DB->perf_get_writes(); 296 } 297 298 if ($warnings) { 299 $warnings = implode("\n", $warnings); 300 trigger_error($warnings, E_USER_WARNING); 301 } 302 } 303 304 /** 305 * Reset all database tables to default values. 306 * @static 307 * @return bool true if reset done, false if skipped 308 */ 309 public static function reset_database() { 310 global $DB; 311 312 if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) { 313 return false; 314 } 315 316 if (!parent::reset_database()) { 317 return false; 318 } 319 320 self::$lastdbwrites = $DB->perf_get_writes(); 321 322 return true; 323 } 324 325 /** 326 * Called during bootstrap only! 327 * @internal 328 * @static 329 * @return void 330 */ 331 public static function bootstrap_init() { 332 global $CFG, $SITE, $DB, $FULLME; 333 334 // backup the globals 335 self::$globals['_SERVER'] = $_SERVER; 336 self::$globals['CFG'] = clone($CFG); 337 self::$globals['SITE'] = clone($SITE); 338 self::$globals['DB'] = $DB; 339 self::$globals['FULLME'] = $FULLME; 340 341 // refresh data in all tables, clear caches, etc. 342 self::reset_all_data(); 343 } 344 345 /** 346 * Print some Moodle related info to console. 347 * @internal 348 * @static 349 * @return void 350 */ 351 public static function bootstrap_moodle_info() { 352 echo self::get_site_info(); 353 } 354 355 /** 356 * Returns original state of global variable. 357 * @static 358 * @param string $name 359 * @return mixed 360 */ 361 public static function get_global_backup($name) { 362 if ($name === 'DB') { 363 // no cloning of database object, 364 // we just need the original reference, not original state 365 return self::$globals['DB']; 366 } 367 if (isset(self::$globals[$name])) { 368 if (is_object(self::$globals[$name])) { 369 $return = clone(self::$globals[$name]); 370 return $return; 371 } else { 372 return self::$globals[$name]; 373 } 374 } 375 return null; 376 } 377 378 /** 379 * Is this site initialised to run unit tests? 380 * 381 * @static 382 * @return int array errorcode=>message, 0 means ok 383 */ 384 public static function testing_ready_problem() { 385 global $DB; 386 387 $localename = self::get_locale_name(); 388 if (setlocale(LC_TIME, $localename) === false) { 389 return array(PHPUNIT_EXITCODE_CONFIGERROR, "Required locale '$localename' is not installed."); 390 } 391 392 if (!self::is_test_site()) { 393 // dataroot was verified in bootstrap, so it must be DB 394 return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix'); 395 } 396 397 $tables = $DB->get_tables(false); 398 if (empty($tables)) { 399 return array(PHPUNIT_EXITCODE_INSTALL, ''); 400 } 401 402 if (!self::is_test_data_updated()) { 403 return array(PHPUNIT_EXITCODE_REINSTALL, ''); 404 } 405 406 return array(0, ''); 407 } 408 409 /** 410 * Drop all test site data. 411 * 412 * Note: To be used from CLI scripts only. 413 * 414 * @static 415 * @param bool $displayprogress if true, this method will echo progress information. 416 * @return void may terminate execution with exit code 417 */ 418 public static function drop_site($displayprogress = false) { 419 global $DB, $CFG; 420 421 if (!self::is_test_site()) { 422 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!'); 423 } 424 425 // Purge dataroot 426 if ($displayprogress) { 427 echo "Purging dataroot:\n"; 428 } 429 430 self::reset_dataroot(); 431 testing_initdataroot($CFG->dataroot, 'phpunit'); 432 433 // Drop all tables. 434 self::drop_database($displayprogress); 435 436 // Drop dataroot. 437 self::drop_dataroot(); 438 } 439 440 /** 441 * Perform a fresh test site installation 442 * 443 * Note: To be used from CLI scripts only. 444 * 445 * @static 446 * @return void may terminate execution with exit code 447 */ 448 public static function install_site() { 449 global $DB, $CFG; 450 451 if (!self::is_test_site()) { 452 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!'); 453 } 454 455 if ($DB->get_tables()) { 456 list($errorcode, $message) = self::testing_ready_problem(); 457 if ($errorcode) { 458 phpunit_bootstrap_error(PHPUNIT_EXITCODE_REINSTALL, 'Database tables already present, Moodle PHPUnit test environment can not be initialised'); 459 } else { 460 phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised'); 461 } 462 } 463 464 $options = array(); 465 $options['adminpass'] = 'admin'; 466 $options['shortname'] = 'phpunit'; 467 $options['fullname'] = 'PHPUnit test site'; 468 469 install_cli_database($options, false); 470 471 // Set the admin email address. 472 $DB->set_field('user', 'email', 'admin@example.com', array('username' => 'admin')); 473 474 // Disable all logging for performance and sanity reasons. 475 set_config('enabled_stores', '', 'tool_log'); 476 477 // We need to keep the installed dataroot filedir files. 478 // So each time we reset the dataroot before running a test, the default files are still installed. 479 self::save_original_data_files(); 480 481 // Store version hash in the database and in a file. 482 self::store_versions_hash(); 483 484 // Store database data and structure. 485 self::store_database_state(); 486 } 487 488 /** 489 * Builds dirroot/phpunit.xml file using defaults from /phpunit.xml.dist 490 * @static 491 * @return bool true means main config file created, false means only dataroot file created 492 */ 493 public static function build_config_file() { 494 global $CFG; 495 496 $template = <<<EOF 497 <testsuite name="@component@_testsuite"> 498 <directory suffix="_test.php">@dir@</directory> 499 </testsuite> 500 501 EOF; 502 $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); 503 504 $suites = ''; 505 $includelists = []; 506 $excludelists = []; 507 508 $subsystems = core_component::get_core_subsystems(); 509 $subsystems['core'] = $CFG->dirroot . '/lib'; 510 foreach ($subsystems as $subsystem => $fulldir) { 511 if (empty($fulldir)) { 512 continue; 513 } 514 if (!file_exists("{$fulldir}/tests/")) { 515 // There are no tests - skip this directory. 516 continue; 517 } 518 519 $dir = substr($fulldir, strlen($CFG->dirroot) + 1); 520 if ($coverageinfo = self::get_coverage_info($fulldir)) { 521 $includelists = array_merge($includelists, $coverageinfo->get_includelists($dir)); 522 $excludelists = array_merge($excludelists, $coverageinfo->get_excludelists($dir)); 523 } 524 } 525 526 $plugintypes = core_component::get_plugin_types(); 527 ksort($plugintypes); 528 foreach (array_keys($plugintypes) as $type) { 529 $plugs = core_component::get_plugin_list($type); 530 ksort($plugs); 531 foreach ($plugs as $plug => $plugindir) { 532 if (!file_exists("{$plugindir}/tests/")) { 533 // There are no tests - skip this directory. 534 continue; 535 } 536 537 $dir = substr($plugindir, strlen($CFG->dirroot) + 1); 538 $testdir = "{$dir}/tests"; 539 $component = "{$type}_{$plug}"; 540 541 $suite = str_replace('@component@', $component, $template); 542 $suite = str_replace('@dir@', $testdir, $suite); 543 544 $suites .= $suite; 545 546 if ($coverageinfo = self::get_coverage_info($plugindir)) { 547 548 $includelists = array_merge($includelists, $coverageinfo->get_includelists($dir)); 549 $excludelists = array_merge($excludelists, $coverageinfo->get_excludelists($dir)); 550 } 551 } 552 } 553 554 // Start a sequence between 100000 and 199000 to ensure each call to init produces 555 // different ids in the database. This reduces the risk that hard coded values will 556 // end up being placed in phpunit or behat test code. 557 $sequencestart = 100000 + mt_rand(0, 99) * 1000; 558 559 $data = preg_replace('| *<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', trim($suites, "\n"), $data, 1); 560 $data = str_replace( 561 '<const name="PHPUNIT_SEQUENCE_START" value=""/>', 562 '<const name="PHPUNIT_SEQUENCE_START" value="' . $sequencestart . '"/>', 563 $data); 564 565 $coverages = self::get_coverage_config($includelists, $excludelists); 566 $data = preg_replace('| *<!--@coveragelist@-->|s', trim($coverages, "\n"), $data); 567 568 $result = false; 569 if (is_writable($CFG->dirroot)) { 570 if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) { 571 testing_fix_file_permissions("$CFG->dirroot/phpunit.xml"); 572 } 573 } 574 575 return (bool)$result; 576 } 577 578 /** 579 * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist 580 * 581 * @static 582 * @return void, stops if can not write files 583 */ 584 public static function build_component_config_files() { 585 global $CFG; 586 587 $template = <<<EOT 588 <testsuites> 589 <testsuite name="@component@_testsuite"> 590 <directory suffix="_test.php">.</directory> 591 </testsuite> 592 </testsuites> 593 EOT; 594 $coveragedefault = <<<EOT 595 <include> 596 <directory suffix=".php">.</directory> 597 </include> 598 <exclude> 599 <directory suffix="_test.php">.</directory> 600 </exclude> 601 EOT; 602 603 // Start a sequence between 100000 and 199000 to ensure each call to init produces 604 // different ids in the database. This reduces the risk that hard coded values will 605 // end up being placed in phpunit or behat test code. 606 $sequencestart = 100000 + mt_rand(0, 99) * 1000; 607 608 // Use the upstream file as source for the distributed configurations 609 $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); 610 $ftemplate = preg_replace('| *<!--All core suites.*</testsuites>|s', '<!--@component_suite@-->', $ftemplate); 611 612 // Gets all the components with tests 613 $components = tests_finder::get_components_with_tests('phpunit'); 614 615 // Create the corresponding phpunit.xml file for each component 616 foreach ($components as $cname => $cpath) { 617 // Calculate the component suite 618 $ctemplate = $template; 619 $ctemplate = str_replace('@component@', $cname, $ctemplate); 620 621 $fcontents = str_replace('<!--@component_suite@-->', $ctemplate, $ftemplate); 622 623 // Check for coverage configurations. 624 if ($coverageinfo = self::get_coverage_info($cpath)) { 625 $coverages = self::get_coverage_config($coverageinfo->get_includelists(''), $coverageinfo->get_excludelists('')); 626 } else { 627 $coverages = $coveragedefault; 628 } 629 $fcontents = preg_replace('| *<!--@coveragelist@-->|s', trim($coverages, "\n"), $fcontents); 630 631 // Apply it to the file template. 632 $fcontents = str_replace( 633 '<const name="PHPUNIT_SEQUENCE_START" value=""/>', 634 '<const name="PHPUNIT_SEQUENCE_START" value="' . $sequencestart . '"/>', 635 $fcontents); 636 637 // fix link to schema 638 $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/'); 639 $fcontents = str_replace('lib/phpunit/', str_repeat('../', $level).'lib/phpunit/', $fcontents); 640 641 // Write the file 642 $result = false; 643 if (is_writable($cpath)) { 644 if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) { 645 testing_fix_file_permissions("$cpath/phpunit.xml"); 646 } 647 } 648 // Problems writing file, throw error 649 if (!$result) { 650 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGWARNING, "Can not create $cpath/phpunit.xml configuration file, verify dir permissions"); 651 } 652 } 653 } 654 655 /** 656 * To be called from debugging() only. 657 * @param string $message 658 * @param int $level 659 * @param string $from 660 */ 661 public static function debugging_triggered($message, $level, $from) { 662 // Store only if debugging triggered from actual test, 663 // we need normal debugging outside of tests to find problems in our phpunit integration. 664 $backtrace = debug_backtrace(); 665 666 foreach ($backtrace as $bt) { 667 if (isset($bt['object']) and is_object($bt['object']) 668 && $bt['object'] instanceof PHPUnit\Framework\TestCase) { 669 $debug = new stdClass(); 670 $debug->message = $message; 671 $debug->level = $level; 672 $debug->from = $from; 673 674 self::$debuggings[] = $debug; 675 676 return true; 677 } 678 } 679 return false; 680 } 681 682 /** 683 * Resets the list of debugging messages. 684 */ 685 public static function reset_debugging() { 686 self::$debuggings = array(); 687 set_debugging(DEBUG_DEVELOPER); 688 } 689 690 /** 691 * Returns all debugging messages triggered during test. 692 * @return array with instances having message, level and stacktrace property. 693 */ 694 public static function get_debugging_messages() { 695 return self::$debuggings; 696 } 697 698 /** 699 * Prints out any debug messages accumulated during test execution. 700 * 701 * @param bool $return true to return the messages or false to print them directly. Default false. 702 * @return bool|string false if no debug messages, true if debug triggered or string of messages 703 */ 704 public static function display_debugging_messages($return = false) { 705 if (empty(self::$debuggings)) { 706 return false; 707 } 708 709 $debugstring = ''; 710 foreach(self::$debuggings as $debug) { 711 $debugstring .= 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n"; 712 } 713 714 if ($return) { 715 return $debugstring; 716 } 717 echo $debugstring; 718 return true; 719 } 720 721 /** 722 * Start message redirection. 723 * 724 * Note: Do not call directly from tests, 725 * use $sink = $this->redirectMessages() instead. 726 * 727 * @return phpunit_message_sink 728 */ 729 public static function start_message_redirection() { 730 if (self::$messagesink) { 731 self::stop_message_redirection(); 732 } 733 self::$messagesink = new phpunit_message_sink(); 734 return self::$messagesink; 735 } 736 737 /** 738 * End message redirection. 739 * 740 * Note: Do not call directly from tests, 741 * use $sink->close() instead. 742 */ 743 public static function stop_message_redirection() { 744 self::$messagesink = null; 745 } 746 747 /** 748 * Are messages redirected to some sink? 749 * 750 * Note: to be called from messagelib.php only! 751 * 752 * @return bool 753 */ 754 public static function is_redirecting_messages() { 755 return !empty(self::$messagesink); 756 } 757 758 /** 759 * To be called from messagelib.php only! 760 * 761 * @param stdClass $message record from messages table 762 * @return bool true means send message, false means message "sent" to sink. 763 */ 764 public static function message_sent($message) { 765 if (self::$messagesink) { 766 self::$messagesink->add_message($message); 767 } 768 } 769 770 /** 771 * Start phpmailer redirection. 772 * 773 * Note: Do not call directly from tests, 774 * use $sink = $this->redirectEmails() instead. 775 * 776 * @return phpunit_phpmailer_sink 777 */ 778 public static function start_phpmailer_redirection() { 779 if (self::$phpmailersink) { 780 // If an existing mailer sink is active, just clear it. 781 self::$phpmailersink->clear(); 782 } else { 783 self::$phpmailersink = new phpunit_phpmailer_sink(); 784 } 785 return self::$phpmailersink; 786 } 787 788 /** 789 * End phpmailer redirection. 790 * 791 * Note: Do not call directly from tests, 792 * use $sink->close() instead. 793 */ 794 public static function stop_phpmailer_redirection() { 795 self::$phpmailersink = null; 796 } 797 798 /** 799 * Are messages for phpmailer redirected to some sink? 800 * 801 * Note: to be called from moodle_phpmailer.php only! 802 * 803 * @return bool 804 */ 805 public static function is_redirecting_phpmailer() { 806 return !empty(self::$phpmailersink); 807 } 808 809 /** 810 * To be called from messagelib.php only! 811 * 812 * @param stdClass $message record from messages table 813 * @return bool true means send message, false means message "sent" to sink. 814 */ 815 public static function phpmailer_sent($message) { 816 if (self::$phpmailersink) { 817 self::$phpmailersink->add_message($message); 818 } 819 } 820 821 /** 822 * Start event redirection. 823 * 824 * @private 825 * Note: Do not call directly from tests, 826 * use $sink = $this->redirectEvents() instead. 827 * 828 * @return phpunit_event_sink 829 */ 830 public static function start_event_redirection() { 831 if (self::$eventsink) { 832 self::stop_event_redirection(); 833 } 834 self::$eventsink = new phpunit_event_sink(); 835 return self::$eventsink; 836 } 837 838 /** 839 * End event redirection. 840 * 841 * @private 842 * Note: Do not call directly from tests, 843 * use $sink->close() instead. 844 */ 845 public static function stop_event_redirection() { 846 self::$eventsink = null; 847 } 848 849 /** 850 * Are events redirected to some sink? 851 * 852 * Note: to be called from \core\event\base only! 853 * 854 * @private 855 * @return bool 856 */ 857 public static function is_redirecting_events() { 858 return !empty(self::$eventsink); 859 } 860 861 /** 862 * To be called from \core\event\base only! 863 * 864 * @private 865 * @param \core\event\base $event record from event_read table 866 * @return bool true means send event, false means event "sent" to sink. 867 */ 868 public static function event_triggered(\core\event\base $event) { 869 if (self::$eventsink) { 870 self::$eventsink->add_event($event); 871 } 872 } 873 874 /** 875 * Gets the name of the locale for testing environment (Australian English) 876 * depending on platform environment. 877 * 878 * @return string the locale name. 879 */ 880 protected static function get_locale_name() { 881 global $CFG; 882 if ($CFG->ostype === 'WINDOWS') { 883 return 'English_Australia.1252'; 884 } else { 885 return 'en_AU.UTF-8'; 886 } 887 } 888 889 /** 890 * Executes all adhoc tasks in the queue. Useful for testing asynchronous behaviour. 891 * 892 * @return void 893 */ 894 public static function run_all_adhoc_tasks() { 895 $now = time(); 896 while (($task = \core\task\manager::get_next_adhoc_task($now)) !== null) { 897 try { 898 $task->execute(); 899 \core\task\manager::adhoc_task_complete($task); 900 } catch (Exception $e) { 901 \core\task\manager::adhoc_task_failed($task); 902 } 903 } 904 } 905 906 /** 907 * Helper function to call a protected/private method of an object using reflection. 908 * 909 * Example 1. Calling a protected object method: 910 * $result = call_internal_method($myobject, 'method_name', [$param1, $param2], '\my\namespace\myobjectclassname'); 911 * 912 * Example 2. Calling a protected static method: 913 * $result = call_internal_method(null, 'method_name', [$param1, $param2], '\my\namespace\myclassname'); 914 * 915 * @param object|null $object the object on which to call the method, or null if calling a static method. 916 * @param string $methodname the name of the protected/private method. 917 * @param array $params the array of function params to pass to the method. 918 * @param string $classname the fully namespaced name of the class the object was created from (base in the case of mocks), 919 * or the name of the static class when calling a static method. 920 * @return mixed the respective return value of the method. 921 */ 922 public static function call_internal_method($object, $methodname, array $params, $classname) { 923 $reflection = new \ReflectionClass($classname); 924 $method = $reflection->getMethod($methodname); 925 $method->setAccessible(true); 926 return $method->invokeArgs($object, $params); 927 } 928 929 /** 930 * Pad the supplied string with $level levels of indentation. 931 * 932 * @param string $string The string to pad 933 * @param int $level The number of levels of indentation to pad 934 * @return string 935 */ 936 protected static function pad(string $string, int $level) : string { 937 return str_repeat(" ", $level * 2) . "{$string}\n"; 938 } 939 940 /** 941 * Get the coverage config for the supplied includelist and excludelist configuration. 942 * 943 * @param array[] $includelists The list of files/folders in the includelist. 944 * @param array[] $excludelists The list of files/folders in the excludelist. 945 * @return string 946 */ 947 protected static function get_coverage_config(array $includelists, array $excludelists) : string { 948 $coverages = ''; 949 if (!empty($includelists)) { 950 $coverages .= self::pad("<include>", 2); 951 foreach ($includelists as $line) { 952 $coverages .= self::pad($line, 3); 953 } 954 $coverages .= self::pad("</include>", 2); 955 if (!empty($excludelists)) { 956 $coverages .= self::pad("<exclude>", 2); 957 foreach ($excludelists as $line) { 958 $coverages .= self::pad($line, 3); 959 } 960 $coverages .= self::pad("</exclude>", 2); 961 } 962 } 963 964 return $coverages; 965 } 966 967 /** 968 * Get the phpunit_coverage_info for the specified plugin or subsystem directory. 969 * 970 * @param string $fulldir The directory to find the coverage info file in. 971 * @return phpunit_coverage_info 972 */ 973 protected static function get_coverage_info(string $fulldir): ?phpunit_coverage_info { 974 $coverageconfig = "{$fulldir}/tests/coverage.php"; 975 if (file_exists($coverageconfig)) { 976 $coverageinfo = require($coverageconfig); 977 if (!$coverageinfo instanceof phpunit_coverage_info) { 978 throw new \coding_exception("{$coverageconfig} does not return a phpunit_coverage_info"); 979 } 980 981 return $coverageinfo; 982 } 983 984 return null; 985 } 986 987 /** 988 * Whether the current process is an isolated test process. 989 * 990 * @return bool 991 */ 992 public static function is_in_isolated_process(): bool { 993 // Note: There is no function to call, or much to go by in order to tell whether we are in an isolated process 994 // during Bootstrap, when this function is called. 995 // We can do so by testing the existence of the wrapper function, but there is nothing set until that point. 996 return function_exists('__phpunit_run_isolated_test'); 997 } 998 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body