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]

   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   * Testing util classes
  19   *
  20   * @abstract
  21   * @package    core
  22   * @category   test
  23   * @copyright  2012 Petr Skoda {@link http://skodak.org}
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  /**
  28   * Utils for test sites creation
  29   *
  30   * @package   core
  31   * @category  test
  32   * @copyright 2012 Petr Skoda {@link http://skodak.org}
  33   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  abstract class testing_util {
  36  
  37      /**
  38       * @var string dataroot (likely to be $CFG->dataroot).
  39       */
  40      private static $dataroot = null;
  41  
  42      /**
  43       * @var testing_data_generator
  44       */
  45      protected static $generator = null;
  46  
  47      /**
  48       * @var string current version hash from php files
  49       */
  50      protected static $versionhash = null;
  51  
  52      /**
  53       * @var array original content of all database tables
  54       */
  55      protected static $tabledata = null;
  56  
  57      /**
  58       * @var array original structure of all database tables
  59       */
  60      protected static $tablestructure = null;
  61  
  62      /**
  63       * @var array keep list of sequenceid used in a table.
  64       */
  65      private static $tablesequences = array();
  66  
  67      /**
  68       * @var array list of updated tables.
  69       */
  70      public static $tableupdated = array();
  71  
  72      /**
  73       * @var array original structure of all database tables
  74       */
  75      protected static $sequencenames = null;
  76  
  77      /**
  78       * @var string name of the json file where we store the list of dataroot files to not reset during reset_dataroot.
  79       */
  80      private static $originaldatafilesjson = 'originaldatafiles.json';
  81  
  82      /**
  83       * @var boolean set to true once $originaldatafilesjson file is created.
  84       */
  85      private static $originaldatafilesjsonadded = false;
  86  
  87      /**
  88       * @var int next sequence value for a single test cycle.
  89       */
  90      protected static $sequencenextstartingid = null;
  91  
  92      /**
  93       * Return the name of the JSON file containing the init filenames.
  94       *
  95       * @static
  96       * @return string
  97       */
  98      public static function get_originaldatafilesjson() {
  99          return self::$originaldatafilesjson;
 100      }
 101  
 102      /**
 103       * Return the dataroot. It's useful when mocking the dataroot when unit testing this class itself.
 104       *
 105       * @static
 106       * @return string the dataroot.
 107       */
 108      public static function get_dataroot() {
 109          global $CFG;
 110  
 111          //  By default it's the test framework dataroot.
 112          if (empty(self::$dataroot)) {
 113              self::$dataroot = $CFG->dataroot;
 114          }
 115  
 116          return self::$dataroot;
 117      }
 118  
 119      /**
 120       * Set the dataroot. It's useful when mocking the dataroot when unit testing this class itself.
 121       *
 122       * @param string $dataroot the dataroot of the test framework.
 123       * @static
 124       */
 125      public static function set_dataroot($dataroot) {
 126          self::$dataroot = $dataroot;
 127      }
 128  
 129      /**
 130       * Returns the testing framework name
 131       * @static
 132       * @return string
 133       */
 134      protected static final function get_framework() {
 135          $classname = get_called_class();
 136          return substr($classname, 0, strpos($classname, '_'));
 137      }
 138  
 139      /**
 140       * Get data generator
 141       * @static
 142       * @return testing_data_generator
 143       */
 144      public static function get_data_generator() {
 145          if (is_null(self::$generator)) {
 146              require_once (__DIR__.'/../generator/lib.php');
 147              self::$generator = new testing_data_generator();
 148          }
 149          return self::$generator;
 150      }
 151  
 152      /**
 153       * Does this site (db and dataroot) appear to be used for production?
 154       * We try very hard to prevent accidental damage done to production servers!!
 155       *
 156       * @static
 157       * @return bool
 158       */
 159      public static function is_test_site() {
 160          global $DB, $CFG;
 161  
 162          $framework = self::get_framework();
 163  
 164          if (!file_exists(self::get_dataroot() . '/' . $framework . 'testdir.txt')) {
 165              // this is already tested in bootstrap script,
 166              // but anyway presence of this file means the dataroot is for testing
 167              return false;
 168          }
 169  
 170          $tables = $DB->get_tables(false);
 171          if ($tables) {
 172              if (!$DB->get_manager()->table_exists('config')) {
 173                  return false;
 174              }
 175              if (!get_config('core', $framework . 'test')) {
 176                  return false;
 177              }
 178          }
 179  
 180          return true;
 181      }
 182  
 183      /**
 184       * Returns whether test database and dataroot were created using the current version codebase
 185       *
 186       * @return bool
 187       */
 188      public static function is_test_data_updated() {
 189          global $DB;
 190  
 191          $framework = self::get_framework();
 192  
 193          $datarootpath = self::get_dataroot() . '/' . $framework;
 194          if (!file_exists($datarootpath . '/tabledata.ser') or !file_exists($datarootpath . '/tablestructure.ser')) {
 195              return false;
 196          }
 197  
 198          if (!file_exists($datarootpath . '/versionshash.txt')) {
 199              return false;
 200          }
 201  
 202          $hash = core_component::get_all_versions_hash();
 203          $oldhash = file_get_contents($datarootpath . '/versionshash.txt');
 204  
 205          if ($hash !== $oldhash) {
 206              return false;
 207          }
 208  
 209          // A direct database request must be used to avoid any possible caching of an older value.
 210          $dbhash = $DB->get_field('config', 'value', array('name' => $framework . 'test'));
 211          if ($hash !== $dbhash) {
 212              return false;
 213          }
 214  
 215          return true;
 216      }
 217  
 218      /**
 219       * Stores the status of the database
 220       *
 221       * Serializes the contents and the structure and
 222       * stores it in the test framework space in dataroot
 223       */
 224      protected static function store_database_state() {
 225          global $DB, $CFG;
 226  
 227          $framework = self::get_framework();
 228  
 229          // store data for all tables
 230          $data = array();
 231          $structure = array();
 232          $tables = $DB->get_tables();
 233          foreach ($tables as $table) {
 234              $columns = $DB->get_columns($table);
 235              $structure[$table] = $columns;
 236              if (isset($columns['id']) and $columns['id']->auto_increment) {
 237                  $data[$table] = $DB->get_records($table, array(), 'id ASC');
 238              } else {
 239                  // there should not be many of these
 240                  $data[$table] = $DB->get_records($table, array());
 241              }
 242          }
 243          $data = serialize($data);
 244          $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
 245          file_put_contents($datafile, $data);
 246          testing_fix_file_permissions($datafile);
 247  
 248          $structure = serialize($structure);
 249          $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
 250          file_put_contents($structurefile, $structure);
 251          testing_fix_file_permissions($structurefile);
 252      }
 253  
 254      /**
 255       * Stores the version hash in both database and dataroot
 256       */
 257      protected static function store_versions_hash() {
 258          global $CFG;
 259  
 260          $framework = self::get_framework();
 261          $hash = core_component::get_all_versions_hash();
 262  
 263          // add test db flag
 264          set_config($framework . 'test', $hash);
 265  
 266          // hash all plugin versions - helps with very fast detection of db structure changes
 267          $hashfile = self::get_dataroot() . '/' . $framework . '/versionshash.txt';
 268          file_put_contents($hashfile, $hash);
 269          testing_fix_file_permissions($hashfile);
 270      }
 271  
 272      /**
 273       * Returns contents of all tables right after installation.
 274       * @static
 275       * @return array  $table=>$records
 276       */
 277      protected static function get_tabledata() {
 278          if (!isset(self::$tabledata)) {
 279              $framework = self::get_framework();
 280  
 281              $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
 282              if (!file_exists($datafile)) {
 283                  // Not initialised yet.
 284                  return array();
 285              }
 286  
 287              $data = file_get_contents($datafile);
 288              self::$tabledata = unserialize($data);
 289          }
 290  
 291          if (!is_array(self::$tabledata)) {
 292              testing_error(1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.');
 293          }
 294  
 295          return self::$tabledata;
 296      }
 297  
 298      /**
 299       * Returns structure of all tables right after installation.
 300       * @static
 301       * @return array $table=>$records
 302       */
 303      public static function get_tablestructure() {
 304          if (!isset(self::$tablestructure)) {
 305              $framework = self::get_framework();
 306  
 307              $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
 308              if (!file_exists($structurefile)) {
 309                  // Not initialised yet.
 310                  return array();
 311              }
 312  
 313              $data = file_get_contents($structurefile);
 314              self::$tablestructure = unserialize($data);
 315          }
 316  
 317          if (!is_array(self::$tablestructure)) {
 318              testing_error(1, 'Can not read dataroot/' . $framework . '/tablestructure.ser or invalid format, reinitialize test database.');
 319          }
 320  
 321          return self::$tablestructure;
 322      }
 323  
 324      /**
 325       * Returns the names of sequences for each autoincrementing id field in all standard tables.
 326       * @static
 327       * @return array $table=>$sequencename
 328       */
 329      public static function get_sequencenames() {
 330          global $DB;
 331  
 332          if (isset(self::$sequencenames)) {
 333              return self::$sequencenames;
 334          }
 335  
 336          if (!$structure = self::get_tablestructure()) {
 337              return array();
 338          }
 339  
 340          self::$sequencenames = array();
 341          foreach ($structure as $table => $ignored) {
 342              $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
 343              if ($name !== false) {
 344                  self::$sequencenames[$table] = $name;
 345              }
 346          }
 347  
 348          return self::$sequencenames;
 349      }
 350  
 351      /**
 352       * Returns list of tables that are unmodified and empty.
 353       *
 354       * @static
 355       * @return array of table names, empty if unknown
 356       */
 357      protected static function guess_unmodified_empty_tables() {
 358          global $DB;
 359  
 360          $dbfamily = $DB->get_dbfamily();
 361  
 362          if ($dbfamily === 'mysql') {
 363              $empties = array();
 364              $prefix = $DB->get_prefix();
 365              $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
 366              foreach ($rs as $info) {
 367                  $table = strtolower($info->name);
 368                  if (strpos($table, $prefix) !== 0) {
 369                      // incorrect table match caused by _
 370                      continue;
 371                  }
 372  
 373                  if (!is_null($info->auto_increment) && $info->rows == 0 && ($info->auto_increment == 1)) {
 374                      $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
 375                      $empties[$table] = $table;
 376                  }
 377              }
 378              $rs->close();
 379              return $empties;
 380  
 381          } else if ($dbfamily === 'mssql') {
 382              $empties = array();
 383              $prefix = $DB->get_prefix();
 384              $sql = "SELECT t.name
 385                        FROM sys.identity_columns i
 386                        JOIN sys.tables t ON t.object_id = i.object_id
 387                       WHERE t.name LIKE ?
 388                         AND i.name = 'id'
 389                         AND i.last_value IS NULL";
 390              $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
 391              foreach ($rs as $info) {
 392                  $table = strtolower($info->name);
 393                  if (strpos($table, $prefix) !== 0) {
 394                      // incorrect table match caused by _
 395                      continue;
 396                  }
 397                  $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
 398                  $empties[$table] = $table;
 399              }
 400              $rs->close();
 401              return $empties;
 402  
 403          } else if ($dbfamily === 'oracle') {
 404              $sequences = self::get_sequencenames();
 405              $sequences = array_map('strtoupper', $sequences);
 406              $lookup = array_flip($sequences);
 407              $empties = array();
 408              list($seqs, $params) = $DB->get_in_or_equal($sequences);
 409              $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
 410              $rs = $DB->get_recordset_sql($sql, $params);
 411              foreach ($rs as $seq) {
 412                  $table = $lookup[$seq->sequence_name];
 413                  $empties[$table] = $table;
 414              }
 415              $rs->close();
 416              return $empties;
 417  
 418          } else {
 419              return array();
 420          }
 421      }
 422  
 423      /**
 424       * Determine the next unique starting id sequences.
 425       *
 426       * @static
 427       * @param array $records The records to use to determine the starting value for the table.
 428       * @param string $table table name.
 429       * @return int The value the sequence should be set to.
 430       */
 431      private static function get_next_sequence_starting_value($records, $table) {
 432          if (isset(self::$tablesequences[$table])) {
 433              return self::$tablesequences[$table];
 434          }
 435  
 436          $id = self::$sequencenextstartingid;
 437  
 438          // If there are records, calculate the minimum id we can use.
 439          // It must be bigger than the last record's id.
 440          if (!empty($records)) {
 441              $lastrecord = end($records);
 442              $id = max($id, $lastrecord->id + 1);
 443          }
 444  
 445          self::$sequencenextstartingid = $id + 1000;
 446  
 447          self::$tablesequences[$table] = $id;
 448  
 449          return $id;
 450      }
 451  
 452      /**
 453       * Reset all database sequences to initial values.
 454       *
 455       * @static
 456       * @param array $empties tables that are known to be unmodified and empty
 457       * @return void
 458       */
 459      public static function reset_all_database_sequences(array $empties = null) {
 460          global $DB;
 461  
 462          if (!$data = self::get_tabledata()) {
 463              // Not initialised yet.
 464              return;
 465          }
 466          if (!$structure = self::get_tablestructure()) {
 467              // Not initialised yet.
 468              return;
 469          }
 470  
 471          $updatedtables = self::$tableupdated;
 472  
 473          // If all starting Id's are the same, it's difficult to detect coding and testing
 474          // errors that use the incorrect id in tests.  The classic case is cmid vs instance id.
 475          // To reduce the chance of the coding error, we start sequences at different values where possible.
 476          // In a attempt to avoid tables with existing id's we start at a high number.
 477          // Reset the value each time all database sequences are reset.
 478          if (defined('PHPUNIT_SEQUENCE_START') and PHPUNIT_SEQUENCE_START) {
 479              self::$sequencenextstartingid = PHPUNIT_SEQUENCE_START;
 480          } else {
 481              self::$sequencenextstartingid = 100000;
 482          }
 483  
 484          $dbfamily = $DB->get_dbfamily();
 485          if ($dbfamily === 'postgres') {
 486              $queries = array();
 487              $prefix = $DB->get_prefix();
 488              foreach ($data as $table => $records) {
 489                  // If table is not modified then no need to do anything.
 490                  if (!isset($updatedtables[$table])) {
 491                      continue;
 492                  }
 493                  if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 494                      $nextid = self::get_next_sequence_starting_value($records, $table);
 495                      $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
 496                  }
 497              }
 498              if ($queries) {
 499                  $DB->change_database_structure(implode(';', $queries));
 500              }
 501  
 502          } else if ($dbfamily === 'mysql') {
 503              $queries = array();
 504              $sequences = array();
 505              $prefix = $DB->get_prefix();
 506              $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
 507              foreach ($rs as $info) {
 508                  $table = strtolower($info->name);
 509                  if (strpos($table, $prefix) !== 0) {
 510                      // incorrect table match caused by _
 511                      continue;
 512                  }
 513                  if (!is_null($info->auto_increment)) {
 514                      $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
 515                      $sequences[$table] = $info->auto_increment;
 516                  }
 517              }
 518              $rs->close();
 519              $prefix = $DB->get_prefix();
 520              foreach ($data as $table => $records) {
 521                  // If table is not modified then no need to do anything.
 522                  if (!isset($updatedtables[$table])) {
 523                      continue;
 524                  }
 525                  if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 526                      if (isset($sequences[$table])) {
 527                          $nextid = self::get_next_sequence_starting_value($records, $table);
 528                          if ($sequences[$table] != $nextid) {
 529                              $queries[] = "ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid";
 530                          }
 531                      } else {
 532                          // some problem exists, fallback to standard code
 533                          $DB->get_manager()->reset_sequence($table);
 534                      }
 535                  }
 536              }
 537              if ($queries) {
 538                  $DB->change_database_structure(implode(';', $queries));
 539              }
 540  
 541          } else if ($dbfamily === 'oracle') {
 542              $sequences = self::get_sequencenames();
 543              $sequences = array_map('strtoupper', $sequences);
 544              $lookup = array_flip($sequences);
 545  
 546              $current = array();
 547              list($seqs, $params) = $DB->get_in_or_equal($sequences);
 548              $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
 549              $rs = $DB->get_recordset_sql($sql, $params);
 550              foreach ($rs as $seq) {
 551                  $table = $lookup[$seq->sequence_name];
 552                  $current[$table] = $seq->last_number;
 553              }
 554              $rs->close();
 555  
 556              foreach ($data as $table => $records) {
 557                  // If table is not modified then no need to do anything.
 558                  if (!isset($updatedtables[$table])) {
 559                      continue;
 560                  }
 561                  if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 562                      $lastrecord = end($records);
 563                      if ($lastrecord) {
 564                          $nextid = $lastrecord->id + 1;
 565                      } else {
 566                          $nextid = 1;
 567                      }
 568                      if (!isset($current[$table])) {
 569                          $DB->get_manager()->reset_sequence($table);
 570                      } else if ($nextid == $current[$table]) {
 571                          continue;
 572                      }
 573                      // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
 574                      $seqname = $sequences[$table];
 575                      $cachesize = $DB->get_manager()->generator->sequence_cache_size;
 576                      $DB->change_database_structure("DROP SEQUENCE $seqname");
 577                      $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
 578                  }
 579              }
 580  
 581          } else {
 582              // note: does mssql support any kind of faster reset?
 583              // This also implies mssql will not use unique sequence values.
 584              if (is_null($empties) and (empty($updatedtables))) {
 585                  $empties = self::guess_unmodified_empty_tables();
 586              }
 587              foreach ($data as $table => $records) {
 588                  // If table is not modified then no need to do anything.
 589                  if (isset($empties[$table]) or (!isset($updatedtables[$table]))) {
 590                      continue;
 591                  }
 592                  if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 593                      $DB->get_manager()->reset_sequence($table);
 594                  }
 595              }
 596          }
 597      }
 598  
 599      /**
 600       * Reset all database tables to default values.
 601       * @static
 602       * @return bool true if reset done, false if skipped
 603       */
 604      public static function reset_database() {
 605          global $DB;
 606  
 607          $tables = $DB->get_tables(false);
 608          if (!$tables or empty($tables['config'])) {
 609              // not installed yet
 610              return false;
 611          }
 612  
 613          if (!$data = self::get_tabledata()) {
 614              // not initialised yet
 615              return false;
 616          }
 617          if (!$structure = self::get_tablestructure()) {
 618              // not initialised yet
 619              return false;
 620          }
 621  
 622          $empties = array();
 623          // Use local copy of self::$tableupdated, as list gets updated in for loop.
 624          $updatedtables = self::$tableupdated;
 625  
 626          // If empty tablesequences list then it's the very first run.
 627          if (empty(self::$tablesequences) && (($DB->get_dbfamily() != 'mysql') && ($DB->get_dbfamily() != 'postgres'))) {
 628              // Only Mysql and Postgres support random sequence, so don't guess, just reset everything on very first run.
 629              $empties = self::guess_unmodified_empty_tables();
 630          }
 631  
 632          // Check if any table has been modified by behat selenium process.
 633          if (defined('BEHAT_SITE_RUNNING')) {
 634              // Crazy way to reset :(.
 635              $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
 636              if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
 637                  self::$tableupdated = array_merge(self::$tableupdated, $tablesupdated);
 638                  unlink($tablesupdatedfile);
 639              }
 640              $updatedtables = self::$tableupdated;
 641          }
 642  
 643          foreach ($data as $table => $records) {
 644              // If table is not modified then no need to do anything.
 645              // $updatedtables tables is set after the first run, so check before checking for specific table update.
 646              if (!empty($updatedtables) && !isset($updatedtables[$table])) {
 647                  continue;
 648              }
 649  
 650              if (empty($records)) {
 651                  if (!isset($empties[$table])) {
 652                      // Table has been modified and is not empty.
 653                      $DB->delete_records($table, array());
 654                  }
 655                  continue;
 656              }
 657  
 658              if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 659                  $currentrecords = $DB->get_records($table, array(), 'id ASC');
 660                  $changed = false;
 661                  foreach ($records as $id => $record) {
 662                      if (!isset($currentrecords[$id])) {
 663                          $changed = true;
 664                          break;
 665                      }
 666                      if ((array)$record != (array)$currentrecords[$id]) {
 667                          $changed = true;
 668                          break;
 669                      }
 670                      unset($currentrecords[$id]);
 671                  }
 672                  if (!$changed) {
 673                      if ($currentrecords) {
 674                          $lastrecord = end($records);
 675                          $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
 676                          continue;
 677                      } else {
 678                          continue;
 679                      }
 680                  }
 681              }
 682  
 683              $DB->delete_records($table, array());
 684              foreach ($records as $record) {
 685                  $DB->import_record($table, $record, false, true);
 686              }
 687          }
 688  
 689          // reset all next record ids - aka sequences
 690          self::reset_all_database_sequences($empties);
 691  
 692          // remove extra tables
 693          foreach ($tables as $table) {
 694              if (!isset($data[$table])) {
 695                  $DB->get_manager()->drop_table(new xmldb_table($table));
 696              }
 697          }
 698  
 699          self::reset_updated_table_list();
 700  
 701          return true;
 702      }
 703  
 704      /**
 705       * Purge dataroot directory
 706       * @static
 707       * @return void
 708       */
 709      public static function reset_dataroot() {
 710          global $CFG;
 711  
 712          $childclassname = self::get_framework() . '_util';
 713  
 714          // Do not delete automatically installed files.
 715          self::skip_original_data_files($childclassname);
 716  
 717          // Clear file status cache, before checking file_exists.
 718          clearstatcache();
 719  
 720          // Clean up the dataroot folder.
 721          $handle = opendir(self::get_dataroot());
 722          while (false !== ($item = readdir($handle))) {
 723              if (in_array($item, $childclassname::$datarootskiponreset)) {
 724                  continue;
 725              }
 726              if (is_dir(self::get_dataroot()."/$item")) {
 727                  remove_dir(self::get_dataroot()."/$item", false);
 728              } else {
 729                  unlink(self::get_dataroot()."/$item");
 730              }
 731          }
 732          closedir($handle);
 733  
 734          // Clean up the dataroot/filedir folder.
 735          if (file_exists(self::get_dataroot() . '/filedir')) {
 736              $handle = opendir(self::get_dataroot() . '/filedir');
 737              while (false !== ($item = readdir($handle))) {
 738                  if (in_array('filedir' . DIRECTORY_SEPARATOR . $item, $childclassname::$datarootskiponreset)) {
 739                      continue;
 740                  }
 741                  if (is_dir(self::get_dataroot()."/filedir/$item")) {
 742                      remove_dir(self::get_dataroot()."/filedir/$item", false);
 743                  } else {
 744                      unlink(self::get_dataroot()."/filedir/$item");
 745                  }
 746              }
 747              closedir($handle);
 748          }
 749  
 750          make_temp_directory('');
 751          make_backup_temp_directory('');
 752          make_cache_directory('');
 753          make_localcache_directory('');
 754          // Purge all data from the caches. This is required for consistency between tests.
 755          // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
 756          // and now we will purge any other caches as well.  This must be done before the cache_factory::reset() as that
 757          // removes all definitions of caches and purge does not have valid caches to operate on.
 758          cache_helper::purge_all();
 759          // Reset the cache API so that it recreates it's required directories as well.
 760          cache_factory::reset();
 761      }
 762  
 763      /**
 764       * Gets a text-based site version description.
 765       *
 766       * @return string The site info
 767       */
 768      public static function get_site_info() {
 769          global $CFG;
 770  
 771          $output = '';
 772  
 773          // All developers have to understand English, do not localise!
 774          $env = self::get_environment();
 775  
 776          $output .= "Moodle ".$env['moodleversion'];
 777          if ($hash = self::get_git_hash()) {
 778              $output .= ", $hash";
 779          }
 780          $output .= "\n";
 781  
 782          // Add php version.
 783          require_once($CFG->libdir.'/environmentlib.php');
 784          $output .= "Php: ". normalize_version($env['phpversion']);
 785  
 786          // Add database type and version.
 787          $output .= ", " . $env['dbtype'] . ": " . $env['dbversion'];
 788  
 789          // OS details.
 790          $output .= ", OS: " . $env['os'] . "\n";
 791  
 792          return $output;
 793      }
 794  
 795      /**
 796       * Try to get current git hash of the Moodle in $CFG->dirroot.
 797       * @return string null if unknown, sha1 hash if known
 798       */
 799      public static function get_git_hash() {
 800          global $CFG;
 801  
 802          // This is a bit naive, but it should mostly work for all platforms.
 803  
 804          if (!file_exists("$CFG->dirroot/.git/HEAD")) {
 805              return null;
 806          }
 807  
 808          $headcontent = file_get_contents("$CFG->dirroot/.git/HEAD");
 809          if ($headcontent === false) {
 810              return null;
 811          }
 812  
 813          $headcontent = trim($headcontent);
 814  
 815          // If it is pointing to a hash we return it directly.
 816          if (strlen($headcontent) === 40) {
 817              return $headcontent;
 818          }
 819  
 820          if (strpos($headcontent, 'ref: ') !== 0) {
 821              return null;
 822          }
 823  
 824          $ref = substr($headcontent, 5);
 825  
 826          if (!file_exists("$CFG->dirroot/.git/$ref")) {
 827              return null;
 828          }
 829  
 830          $hash = file_get_contents("$CFG->dirroot/.git/$ref");
 831  
 832          if ($hash === false) {
 833              return null;
 834          }
 835  
 836          $hash = trim($hash);
 837  
 838          if (strlen($hash) != 40) {
 839              return null;
 840          }
 841  
 842          return $hash;
 843      }
 844  
 845      /**
 846       * Set state of modified tables.
 847       *
 848       * @param string $sql sql which is updating the table.
 849       */
 850      public static function set_table_modified_by_sql($sql) {
 851          global $DB;
 852  
 853          $prefix = $DB->get_prefix();
 854  
 855          preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches);
 856          // Ignore random sql for testing like "XXUPDATE SET XSSD".
 857          if (!empty($matches[1])) {
 858              $table = trim($matches[1]);
 859              $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table);
 860              self::$tableupdated[$table] = true;
 861  
 862              if (defined('BEHAT_SITE_RUNNING')) {
 863                  $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
 864                  $tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true);
 865                  if (!isset($tablesupdated[$table])) {
 866                      $tablesupdated[$table] = true;
 867                      @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT));
 868                  }
 869              }
 870          }
 871      }
 872  
 873      /**
 874       * Reset updated table list. This should be done after every reset.
 875       */
 876      public static function reset_updated_table_list() {
 877          self::$tableupdated = array();
 878      }
 879  
 880      /**
 881       * Delete tablesupdatedbyscenario file. This should be called before suite,
 882       * to ensure full db reset.
 883       */
 884      public static function clean_tables_updated_by_scenario_list() {
 885          $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
 886          if (file_exists($tablesupdatedfile)) {
 887              unlink($tablesupdatedfile);
 888          }
 889  
 890          // Reset static cache of cli process.
 891          self::reset_updated_table_list();
 892      }
 893  
 894      /**
 895       * Returns the path to the file which holds list of tables updated in scenario.
 896       * @return string
 897       */
 898      protected final static function get_tables_updated_by_scenario_list_path() {
 899          return self::get_dataroot() . '/tablesupdatedbyscenario.json';
 900      }
 901  
 902      /**
 903       * Drop the whole test database
 904       * @static
 905       * @param bool $displayprogress
 906       */
 907      protected static function drop_database($displayprogress = false) {
 908          global $DB;
 909  
 910          $tables = $DB->get_tables(false);
 911          if (isset($tables['config'])) {
 912              // config always last to prevent problems with interrupted drops!
 913              unset($tables['config']);
 914              $tables['config'] = 'config';
 915          }
 916  
 917          if ($displayprogress) {
 918              echo "Dropping tables:\n";
 919          }
 920          $dotsonline = 0;
 921          foreach ($tables as $tablename) {
 922              $table = new xmldb_table($tablename);
 923              $DB->get_manager()->drop_table($table);
 924  
 925              if ($dotsonline == 60) {
 926                  if ($displayprogress) {
 927                      echo "\n";
 928                  }
 929                  $dotsonline = 0;
 930              }
 931              if ($displayprogress) {
 932                  echo '.';
 933              }
 934              $dotsonline += 1;
 935          }
 936          if ($displayprogress) {
 937              echo "\n";
 938          }
 939      }
 940  
 941      /**
 942       * Drops the test framework dataroot
 943       * @static
 944       */
 945      protected static function drop_dataroot() {
 946          global $CFG;
 947  
 948          $framework = self::get_framework();
 949          $childclassname = $framework . '_util';
 950  
 951          $files = scandir(self::get_dataroot() . '/'  . $framework);
 952          foreach ($files as $file) {
 953              if (in_array($file, $childclassname::$datarootskipondrop)) {
 954                  continue;
 955              }
 956              $path = self::get_dataroot() . '/' . $framework . '/' . $file;
 957              if (is_dir($path)) {
 958                  remove_dir($path, false);
 959              } else {
 960                  unlink($path);
 961              }
 962          }
 963  
 964          $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
 965          if (file_exists($jsonfilepath)) {
 966              // Delete the json file.
 967              unlink($jsonfilepath);
 968              // Delete the dataroot filedir.
 969              remove_dir(self::get_dataroot() . '/filedir', false);
 970          }
 971      }
 972  
 973      /**
 974       * Skip the original dataroot files to not been reset.
 975       *
 976       * @static
 977       * @param string $utilclassname the util class name..
 978       */
 979      protected static function skip_original_data_files($utilclassname) {
 980          $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
 981          if (file_exists($jsonfilepath)) {
 982  
 983              $listfiles = file_get_contents($jsonfilepath);
 984  
 985              // Mark each files as to not be reset.
 986              if (!empty($listfiles) && !self::$originaldatafilesjsonadded) {
 987                  $originaldatarootfiles = json_decode($listfiles);
 988                  // Keep the json file. Only drop_dataroot() should delete it.
 989                  $originaldatarootfiles[] = self::$originaldatafilesjson;
 990                  $utilclassname::$datarootskiponreset = array_merge($utilclassname::$datarootskiponreset,
 991                      $originaldatarootfiles);
 992                  self::$originaldatafilesjsonadded = true;
 993              }
 994          }
 995      }
 996  
 997      /**
 998       * Save the list of the original dataroot files into a json file.
 999       */
1000      protected static function save_original_data_files() {
1001          global $CFG;
1002  
1003          $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1004  
1005          // Save the original dataroot files if not done (only executed the first time).
1006          if (!file_exists($jsonfilepath)) {
1007  
1008              $listfiles = array();
1009              $currentdir = 'filedir' . DIRECTORY_SEPARATOR . '.';
1010              $parentdir = 'filedir' . DIRECTORY_SEPARATOR . '..';
1011              $listfiles[$currentdir] = $currentdir;
1012              $listfiles[$parentdir] = $parentdir;
1013  
1014              $filedir = self::get_dataroot() . '/filedir';
1015              if (file_exists($filedir)) {
1016                  $directory = new RecursiveDirectoryIterator($filedir);
1017                  foreach (new RecursiveIteratorIterator($directory) as $file) {
1018                      if ($file->isDir()) {
1019                          $key = substr($file->getPath(), strlen(self::get_dataroot() . '/'));
1020                      } else {
1021                          $key = substr($file->getPathName(), strlen(self::get_dataroot() . '/'));
1022                      }
1023                      $listfiles[$key] = $key;
1024                  }
1025              }
1026  
1027              // Save the file list in a JSON file.
1028              $fp = fopen($jsonfilepath, 'w');
1029              fwrite($fp, json_encode(array_values($listfiles)));
1030              fclose($fp);
1031          }
1032      }
1033  
1034      /**
1035       * Return list of environment versions on which tests will run.
1036       * Environment includes:
1037       * - moodleversion
1038       * - phpversion
1039       * - dbtype
1040       * - dbversion
1041       * - os
1042       *
1043       * @return array
1044       */
1045      public static function get_environment() {
1046          global $CFG, $DB;
1047  
1048          $env = array();
1049  
1050          // Add moodle version.
1051          $release = null;
1052          require("$CFG->dirroot/version.php");
1053          $env['moodleversion'] = $release;
1054  
1055          // Add php version.
1056          $phpversion = phpversion();
1057          $env['phpversion'] = $phpversion;
1058  
1059          // Add database type and version.
1060          $dbtype = $CFG->dbtype;
1061          $dbinfo = $DB->get_server_info();
1062          $dbversion = $dbinfo['version'];
1063          $env['dbtype'] = $dbtype;
1064          $env['dbversion'] = $dbversion;
1065  
1066          // OS details.
1067          $osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m');
1068          $env['os'] = $osdetails;
1069  
1070          return $env;
1071      }
1072  }