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