Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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

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