Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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