Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]

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