Search moodle.org's
Developer Documentation

See Release Notes

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

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