Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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