Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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