Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * 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          $borkedmysql = false;
 644          if ($DB->get_dbfamily() === 'mysql') {
 645              $version = $DB->get_server_info();
 646              if (version_compare($version['version'], '5.7.4', '<')) {
 647                  // Everything that comes from Oracle is evil!
 648                  //
 649                  // See http://dev.mysql.com/doc/refman/5.6/en/alter-table.html
 650                  // You cannot reset the counter to a value less than or equal to to the value that is currently in use.
 651                  //
 652                  // From 5.6.16 release notes:
 653                  //   InnoDB: The ALTER TABLE INPLACE algorithm would fail to decrease the auto-increment value.
 654                  //           (Bug #17250787, Bug #69882)
 655                  // This also impacts MySQL < 5.7.4.
 656                  $borkedmysql = true;
 657              }
 658  
 659              if ($borkedmysql) {
 660                  $mysqlsequences = array();
 661                  $prefix = $DB->get_prefix();
 662                  $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
 663                  foreach ($rs as $info) {
 664                      $table = strtolower($info->name);
 665                      if (strpos($table, $prefix) !== 0) {
 666                          // Incorrect table match caused by _ char.
 667                          continue;
 668                      }
 669                      if (!is_null($info->auto_increment)) {
 670                          $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
 671                          $mysqlsequences[$table] = $info->auto_increment;
 672                      }
 673                  }
 674                  $rs->close();
 675              }
 676          }
 677  
 678          foreach ($data as $table => $records) {
 679              // If table is not modified then no need to do anything.
 680              // $updatedtables tables is set after the first run, so check before checking for specific table update.
 681              if (!empty($updatedtables) && !isset($updatedtables[$table])) {
 682                  continue;
 683              }
 684  
 685              if ($borkedmysql) {
 686                  if (empty($records)) {
 687                      if (!isset($empties[$table])) {
 688                          // Table has been modified and is not empty.
 689                          $DB->delete_records($table, null);
 690                      }
 691                      continue;
 692                  }
 693  
 694                  if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 695                      $current = $DB->get_records($table, array(), 'id ASC');
 696                      if ($current == $records) {
 697                          if (isset($mysqlsequences[$table]) and $mysqlsequences[$table] == $structure[$table]['id']->auto_increment) {
 698                              continue;
 699                          }
 700                      }
 701                  }
 702  
 703                  // Use TRUNCATE as a workaround and reinsert everything.
 704                  $DB->delete_records($table, null);
 705                  foreach ($records as $record) {
 706                      $DB->import_record($table, $record, false, true);
 707                  }
 708                  continue;
 709              }
 710  
 711              if (empty($records)) {
 712                  if (!isset($empties[$table])) {
 713                      // Table has been modified and is not empty.
 714                      $DB->delete_records($table, array());
 715                  }
 716                  continue;
 717              }
 718  
 719              if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 720                  $currentrecords = $DB->get_records($table, array(), 'id ASC');
 721                  $changed = false;
 722                  foreach ($records as $id => $record) {
 723                      if (!isset($currentrecords[$id])) {
 724                          $changed = true;
 725                          break;
 726                      }
 727                      if ((array)$record != (array)$currentrecords[$id]) {
 728                          $changed = true;
 729                          break;
 730                      }
 731                      unset($currentrecords[$id]);
 732                  }
 733                  if (!$changed) {
 734                      if ($currentrecords) {
 735                          $lastrecord = end($records);
 736                          $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
 737                          continue;
 738                      } else {
 739                          continue;
 740                      }
 741                  }
 742              }
 743  
 744              $DB->delete_records($table, array());
 745              foreach ($records as $record) {
 746                  $DB->import_record($table, $record, false, true);
 747              }
 748          }
 749  
 750          // reset all next record ids - aka sequences
 751          self::reset_all_database_sequences($empties);
 752  
 753          // remove extra tables
 754          foreach ($tables as $table) {
 755              if (!isset($data[$table])) {
 756                  $DB->get_manager()->drop_table(new xmldb_table($table));
 757              }
 758          }
 759  
 760          self::reset_updated_table_list();
 761  
 762          return true;
 763      }
 764  
 765      /**
 766       * Purge dataroot directory
 767       * @static
 768       * @return void
 769       */
 770      public static function reset_dataroot() {
 771          global $CFG;
 772  
 773          $childclassname = self::get_framework() . '_util';
 774  
 775          // Do not delete automatically installed files.
 776          self::skip_original_data_files($childclassname);
 777  
 778          // Clear file status cache, before checking file_exists.
 779          clearstatcache();
 780  
 781          // Clean up the dataroot folder.
 782          $handle = opendir(self::get_dataroot());
 783          while (false !== ($item = readdir($handle))) {
 784              if (in_array($item, $childclassname::$datarootskiponreset)) {
 785                  continue;
 786              }
 787              if (is_dir(self::get_dataroot()."/$item")) {
 788                  remove_dir(self::get_dataroot()."/$item", false);
 789              } else {
 790                  unlink(self::get_dataroot()."/$item");
 791              }
 792          }
 793          closedir($handle);
 794  
 795          // Clean up the dataroot/filedir folder.
 796          if (file_exists(self::get_dataroot() . '/filedir')) {
 797              $handle = opendir(self::get_dataroot() . '/filedir');
 798              while (false !== ($item = readdir($handle))) {
 799                  if (in_array('filedir' . DIRECTORY_SEPARATOR . $item, $childclassname::$datarootskiponreset)) {
 800                      continue;
 801                  }
 802                  if (is_dir(self::get_dataroot()."/filedir/$item")) {
 803                      remove_dir(self::get_dataroot()."/filedir/$item", false);
 804                  } else {
 805                      unlink(self::get_dataroot()."/filedir/$item");
 806                  }
 807              }
 808              closedir($handle);
 809          }
 810  
 811          make_temp_directory('');
 812          make_backup_temp_directory('');
 813          make_cache_directory('');
 814          make_localcache_directory('');
 815          // Purge all data from the caches. This is required for consistency between tests.
 816          // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
 817          // and now we will purge any other caches as well.  This must be done before the cache_factory::reset() as that
 818          // removes all definitions of caches and purge does not have valid caches to operate on.
 819          cache_helper::purge_all();
 820          // Reset the cache API so that it recreates it's required directories as well.
 821          cache_factory::reset();
 822      }
 823  
 824      /**
 825       * Gets a text-based site version description.
 826       *
 827       * @return string The site info
 828       */
 829      public static function get_site_info() {
 830          global $CFG;
 831  
 832          $output = '';
 833  
 834          // All developers have to understand English, do not localise!
 835          $env = self::get_environment();
 836  
 837          $output .= "Moodle ".$env['moodleversion'];
 838          if ($hash = self::get_git_hash()) {
 839              $output .= ", $hash";
 840          }
 841          $output .= "\n";
 842  
 843          // Add php version.
 844          require_once($CFG->libdir.'/environmentlib.php');
 845          $output .= "Php: ". normalize_version($env['phpversion']);
 846  
 847          // Add database type and version.
 848          $output .= ", " . $env['dbtype'] . ": " . $env['dbversion'];
 849  
 850          // OS details.
 851          $output .= ", OS: " . $env['os'] . "\n";
 852  
 853          return $output;
 854      }
 855  
 856      /**
 857       * Try to get current git hash of the Moodle in $CFG->dirroot.
 858       * @return string null if unknown, sha1 hash if known
 859       */
 860      public static function get_git_hash() {
 861          global $CFG;
 862  
 863          // This is a bit naive, but it should mostly work for all platforms.
 864  
 865          if (!file_exists("$CFG->dirroot/.git/HEAD")) {
 866              return null;
 867          }
 868  
 869          $headcontent = file_get_contents("$CFG->dirroot/.git/HEAD");
 870          if ($headcontent === false) {
 871              return null;
 872          }
 873  
 874          $headcontent = trim($headcontent);
 875  
 876          // If it is pointing to a hash we return it directly.
 877          if (strlen($headcontent) === 40) {
 878              return $headcontent;
 879          }
 880  
 881          if (strpos($headcontent, 'ref: ') !== 0) {
 882              return null;
 883          }
 884  
 885          $ref = substr($headcontent, 5);
 886  
 887          if (!file_exists("$CFG->dirroot/.git/$ref")) {
 888              return null;
 889          }
 890  
 891          $hash = file_get_contents("$CFG->dirroot/.git/$ref");
 892  
 893          if ($hash === false) {
 894              return null;
 895          }
 896  
 897          $hash = trim($hash);
 898  
 899          if (strlen($hash) != 40) {
 900              return null;
 901          }
 902  
 903          return $hash;
 904      }
 905  
 906      /**
 907       * Set state of modified tables.
 908       *
 909       * @param string $sql sql which is updating the table.
 910       */
 911      public static function set_table_modified_by_sql($sql) {
 912          global $DB;
 913  
 914          $prefix = $DB->get_prefix();
 915  
 916          preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches);
 917          // Ignore random sql for testing like "XXUPDATE SET XSSD".
 918          if (!empty($matches[1])) {
 919              $table = trim($matches[1]);
 920              $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table);
 921              self::$tableupdated[$table] = true;
 922  
 923              if (defined('BEHAT_SITE_RUNNING')) {
 924                  $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
 925                  $tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true);
 926                  if (!isset($tablesupdated[$table])) {
 927                      $tablesupdated[$table] = true;
 928                      @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT));
 929                  }
 930              }
 931          }
 932      }
 933  
 934      /**
 935       * Reset updated table list. This should be done after every reset.
 936       */
 937      public static function reset_updated_table_list() {
 938          self::$tableupdated = array();
 939      }
 940  
 941      /**
 942       * Delete tablesupdatedbyscenario file. This should be called before suite,
 943       * to ensure full db reset.
 944       */
 945      public static function clean_tables_updated_by_scenario_list() {
 946          $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
 947          if (file_exists($tablesupdatedfile)) {
 948              unlink($tablesupdatedfile);
 949          }
 950  
 951          // Reset static cache of cli process.
 952          self::reset_updated_table_list();
 953      }
 954  
 955      /**
 956       * Returns the path to the file which holds list of tables updated in scenario.
 957       * @return string
 958       */
 959      protected final static function get_tables_updated_by_scenario_list_path() {
 960          return self::get_dataroot() . '/tablesupdatedbyscenario.json';
 961      }
 962  
 963      /**
 964       * Drop the whole test database
 965       * @static
 966       * @param bool $displayprogress
 967       */
 968      protected static function drop_database($displayprogress = false) {
 969          global $DB;
 970  
 971          $tables = $DB->get_tables(false);
 972          if (isset($tables['config'])) {
 973              // config always last to prevent problems with interrupted drops!
 974              unset($tables['config']);
 975              $tables['config'] = 'config';
 976          }
 977  
 978          if ($displayprogress) {
 979              echo "Dropping tables:\n";
 980          }
 981          $dotsonline = 0;
 982          foreach ($tables as $tablename) {
 983              $table = new xmldb_table($tablename);
 984              $DB->get_manager()->drop_table($table);
 985  
 986              if ($dotsonline == 60) {
 987                  if ($displayprogress) {
 988                      echo "\n";
 989                  }
 990                  $dotsonline = 0;
 991              }
 992              if ($displayprogress) {
 993                  echo '.';
 994              }
 995              $dotsonline += 1;
 996          }
 997          if ($displayprogress) {
 998              echo "\n";
 999          }
1000      }
1001  
1002      /**
1003       * Drops the test framework dataroot
1004       * @static
1005       */
1006      protected static function drop_dataroot() {
1007          global $CFG;
1008  
1009          $framework = self::get_framework();
1010          $childclassname = $framework . '_util';
1011  
1012          $files = scandir(self::get_dataroot() . '/'  . $framework);
1013          foreach ($files as $file) {
1014              if (in_array($file, $childclassname::$datarootskipondrop)) {
1015                  continue;
1016              }
1017              $path = self::get_dataroot() . '/' . $framework . '/' . $file;
1018              if (is_dir($path)) {
1019                  remove_dir($path, false);
1020              } else {
1021                  unlink($path);
1022              }
1023          }
1024  
1025          $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1026          if (file_exists($jsonfilepath)) {
1027              // Delete the json file.
1028              unlink($jsonfilepath);
1029              // Delete the dataroot filedir.
1030              remove_dir(self::get_dataroot() . '/filedir', false);
1031          }
1032      }
1033  
1034      /**
1035       * Skip the original dataroot files to not been reset.
1036       *
1037       * @static
1038       * @param string $utilclassname the util class name..
1039       */
1040      protected static function skip_original_data_files($utilclassname) {
1041          $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1042          if (file_exists($jsonfilepath)) {
1043  
1044              $listfiles = file_get_contents($jsonfilepath);
1045  
1046              // Mark each files as to not be reset.
1047              if (!empty($listfiles) && !self::$originaldatafilesjsonadded) {
1048                  $originaldatarootfiles = json_decode($listfiles);
1049                  // Keep the json file. Only drop_dataroot() should delete it.
1050                  $originaldatarootfiles[] = self::$originaldatafilesjson;
1051                  $utilclassname::$datarootskiponreset = array_merge($utilclassname::$datarootskiponreset,
1052                      $originaldatarootfiles);
1053                  self::$originaldatafilesjsonadded = true;
1054              }
1055          }
1056      }
1057  
1058      /**
1059       * Save the list of the original dataroot files into a json file.
1060       */
1061      protected static function save_original_data_files() {
1062          global $CFG;
1063  
1064          $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1065  
1066          // Save the original dataroot files if not done (only executed the first time).
1067          if (!file_exists($jsonfilepath)) {
1068  
1069              $listfiles = array();
1070              $currentdir = 'filedir' . DIRECTORY_SEPARATOR . '.';
1071              $parentdir = 'filedir' . DIRECTORY_SEPARATOR . '..';
1072              $listfiles[$currentdir] = $currentdir;
1073              $listfiles[$parentdir] = $parentdir;
1074  
1075              $filedir = self::get_dataroot() . '/filedir';
1076              if (file_exists($filedir)) {
1077                  $directory = new RecursiveDirectoryIterator($filedir);
1078                  foreach (new RecursiveIteratorIterator($directory) as $file) {
1079                      if ($file->isDir()) {
1080                          $key = substr($file->getPath(), strlen(self::get_dataroot() . '/'));
1081                      } else {
1082                          $key = substr($file->getPathName(), strlen(self::get_dataroot() . '/'));
1083                      }
1084                      $listfiles[$key] = $key;
1085                  }
1086              }
1087  
1088              // Save the file list in a JSON file.
1089              $fp = fopen($jsonfilepath, 'w');
1090              fwrite($fp, json_encode(array_values($listfiles)));
1091              fclose($fp);
1092          }
1093      }
1094  
1095      /**
1096       * Return list of environment versions on which tests will run.
1097       * Environment includes:
1098       * - moodleversion
1099       * - phpversion
1100       * - dbtype
1101       * - dbversion
1102       * - os
1103       *
1104       * @return array
1105       */
1106      public static function get_environment() {
1107          global $CFG, $DB;
1108  
1109          $env = array();
1110  
1111          // Add moodle version.
1112          $release = null;
1113          require("$CFG->dirroot/version.php");
1114          $env['moodleversion'] = $release;
1115  
1116          // Add php version.
1117          $phpversion = phpversion();
1118          $env['phpversion'] = $phpversion;
1119  
1120          // Add database type and version.
1121          $dbtype = $CFG->dbtype;
1122          $dbinfo = $DB->get_server_info();
1123          $dbversion = $dbinfo['version'];
1124          $env['dbtype'] = $dbtype;
1125          $env['dbversion'] = $dbversion;
1126  
1127          // OS details.
1128          $osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m');
1129          $env['os'] = $osdetails;
1130  
1131          return $env;
1132      }
1133  }