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