[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/testing/classes/ -> util.php (source)

   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 $CFG;
 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          $dbhash = get_config('core', $framework . 'test');
 210          if ($hash !== $dbhash) {
 211              return false;
 212          }
 213  
 214          return true;
 215      }
 216  
 217      /**
 218       * Stores the status of the database
 219       *
 220       * Serializes the contents and the structure and
 221       * stores it in the test framework space in dataroot
 222       */
 223      protected static function store_database_state() {
 224          global $DB, $CFG;
 225  
 226          $framework = self::get_framework();
 227  
 228          // store data for all tables
 229          $data = array();
 230          $structure = array();
 231          $tables = $DB->get_tables();
 232          foreach ($tables as $table) {
 233              $columns = $DB->get_columns($table);
 234              $structure[$table] = $columns;
 235              if (isset($columns['id']) and $columns['id']->auto_increment) {
 236                  $data[$table] = $DB->get_records($table, array(), 'id ASC');
 237              } else {
 238                  // there should not be many of these
 239                  $data[$table] = $DB->get_records($table, array());
 240              }
 241          }
 242          $data = serialize($data);
 243          $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
 244          file_put_contents($datafile, $data);
 245          testing_fix_file_permissions($datafile);
 246  
 247          $structure = serialize($structure);
 248          $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
 249          file_put_contents($structurefile, $structure);
 250          testing_fix_file_permissions($structurefile);
 251      }
 252  
 253      /**
 254       * Stores the version hash in both database and dataroot
 255       */
 256      protected static function store_versions_hash() {
 257          global $CFG;
 258  
 259          $framework = self::get_framework();
 260          $hash = core_component::get_all_versions_hash();
 261  
 262          // add test db flag
 263          set_config($framework . 'test', $hash);
 264  
 265          // hash all plugin versions - helps with very fast detection of db structure changes
 266          $hashfile = self::get_dataroot() . '/' . $framework . '/versionshash.txt';
 267          file_put_contents($hashfile, $hash);
 268          testing_fix_file_permissions($hashfile);
 269      }
 270  
 271      /**
 272       * Returns contents of all tables right after installation.
 273       * @static
 274       * @return array  $table=>$records
 275       */
 276      protected static function get_tabledata() {
 277          if (!isset(self::$tabledata)) {
 278              $framework = self::get_framework();
 279  
 280              $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
 281              if (!file_exists($datafile)) {
 282                  // Not initialised yet.
 283                  return array();
 284              }
 285  
 286              $data = file_get_contents($datafile);
 287              self::$tabledata = unserialize($data);
 288          }
 289  
 290          if (!is_array(self::$tabledata)) {
 291              testing_error(1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.');
 292          }
 293  
 294          return self::$tabledata;
 295      }
 296  
 297      /**
 298       * Returns structure of all tables right after installation.
 299       * @static
 300       * @return array $table=>$records
 301       */
 302      public static function get_tablestructure() {
 303          if (!isset(self::$tablestructure)) {
 304              $framework = self::get_framework();
 305  
 306              $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
 307              if (!file_exists($structurefile)) {
 308                  // Not initialised yet.
 309                  return array();
 310              }
 311  
 312              $data = file_get_contents($structurefile);
 313              self::$tablestructure = unserialize($data);
 314          }
 315  
 316          if (!is_array(self::$tablestructure)) {
 317              testing_error(1, 'Can not read dataroot/' . $framework . '/tablestructure.ser or invalid format, reinitialize test database.');
 318          }
 319  
 320          return self::$tablestructure;
 321      }
 322  
 323      /**
 324       * Returns the names of sequences for each autoincrementing id field in all standard tables.
 325       * @static
 326       * @return array $table=>$sequencename
 327       */
 328      public static function get_sequencenames() {
 329          global $DB;
 330  
 331          if (isset(self::$sequencenames)) {
 332              return self::$sequencenames;
 333          }
 334  
 335          if (!$structure = self::get_tablestructure()) {
 336              return array();
 337          }
 338  
 339          self::$sequencenames = array();
 340          foreach ($structure as $table => $ignored) {
 341              $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
 342              if ($name !== false) {
 343                  self::$sequencenames[$table] = $name;
 344              }
 345          }
 346  
 347          return self::$sequencenames;
 348      }
 349  
 350      /**
 351       * Returns list of tables that are unmodified and empty.
 352       *
 353       * @static
 354       * @return array of table names, empty if unknown
 355       */
 356      protected static function guess_unmodified_empty_tables() {
 357          global $DB;
 358  
 359          $dbfamily = $DB->get_dbfamily();
 360  
 361          if ($dbfamily === 'mysql') {
 362              $empties = array();
 363              $prefix = $DB->get_prefix();
 364              $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
 365              foreach ($rs as $info) {
 366                  $table = strtolower($info->name);
 367                  if (strpos($table, $prefix) !== 0) {
 368                      // incorrect table match caused by _
 369                      continue;
 370                  }
 371  
 372                  if (!is_null($info->auto_increment) && $info->rows == 0 && ($info->auto_increment == 1)) {
 373                      $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
 374                      $empties[$table] = $table;
 375                  }
 376              }
 377              $rs->close();
 378              return $empties;
 379  
 380          } else if ($dbfamily === 'mssql') {
 381              $empties = array();
 382              $prefix = $DB->get_prefix();
 383              $sql = "SELECT t.name
 384                        FROM sys.identity_columns i
 385                        JOIN sys.tables t ON t.object_id = i.object_id
 386                       WHERE t.name LIKE ?
 387                         AND i.name = 'id'
 388                         AND i.last_value IS NULL";
 389              $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
 390              foreach ($rs as $info) {
 391                  $table = strtolower($info->name);
 392                  if (strpos($table, $prefix) !== 0) {
 393                      // incorrect table match caused by _
 394                      continue;
 395                  }
 396                  $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
 397                  $empties[$table] = $table;
 398              }
 399              $rs->close();
 400              return $empties;
 401  
 402          } else if ($dbfamily === 'oracle') {
 403              $sequences = self::get_sequencenames();
 404              $sequences = array_map('strtoupper', $sequences);
 405              $lookup = array_flip($sequences);
 406              $empties = array();
 407              list($seqs, $params) = $DB->get_in_or_equal($sequences);
 408              $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
 409              $rs = $DB->get_recordset_sql($sql, $params);
 410              foreach ($rs as $seq) {
 411                  $table = $lookup[$seq->sequence_name];
 412                  $empties[$table] = $table;
 413              }
 414              $rs->close();
 415              return $empties;
 416  
 417          } else {
 418              return array();
 419          }
 420      }
 421  
 422      /**
 423       * Determine the next unique starting id sequences.
 424       *
 425       * @static
 426       * @param array $records The records to use to determine the starting value for the table.
 427       * @param string $table table name.
 428       * @return int The value the sequence should be set to.
 429       */
 430      private static function get_next_sequence_starting_value($records, $table) {
 431          if (isset(self::$tablesequences[$table])) {
 432              return self::$tablesequences[$table];
 433          }
 434  
 435          $id = self::$sequencenextstartingid;
 436  
 437          // If there are records, calculate the minimum id we can use.
 438          // It must be bigger than the last record's id.
 439          if (!empty($records)) {
 440              $lastrecord = end($records);
 441              $id = max($id, $lastrecord->id + 1);
 442          }
 443  
 444          self::$sequencenextstartingid = $id + 1000;
 445  
 446          self::$tablesequences[$table] = $id;
 447  
 448          return $id;
 449      }
 450  
 451      /**
 452       * Reset all database sequences to initial values.
 453       *
 454       * @static
 455       * @param array $empties tables that are known to be unmodified and empty
 456       * @return void
 457       */
 458      public static function reset_all_database_sequences(array $empties = null) {
 459          global $DB;
 460  
 461          if (!$data = self::get_tabledata()) {
 462              // Not initialised yet.
 463              return;
 464          }
 465          if (!$structure = self::get_tablestructure()) {
 466              // Not initialised yet.
 467              return;
 468          }
 469  
 470          $updatedtables = self::$tableupdated;
 471  
 472          // If all starting Id's are the same, it's difficult to detect coding and testing
 473          // errors that use the incorrect id in tests.  The classic case is cmid vs instance id.
 474          // To reduce the chance of the coding error, we start sequences at different values where possible.
 475          // In a attempt to avoid tables with existing id's we start at a high number.
 476          // Reset the value each time all database sequences are reset.
 477          if (defined('PHPUNIT_SEQUENCE_START') and PHPUNIT_SEQUENCE_START) {
 478              self::$sequencenextstartingid = PHPUNIT_SEQUENCE_START;
 479          } else {
 480              self::$sequencenextstartingid = 100000;
 481          }
 482  
 483          $dbfamily = $DB->get_dbfamily();
 484          if ($dbfamily === 'postgres') {
 485              $queries = array();
 486              $prefix = $DB->get_prefix();
 487              foreach ($data as $table => $records) {
 488                  // If table is not modified then no need to do anything.
 489                  if (!isset($updatedtables[$table])) {
 490                      continue;
 491                  }
 492                  if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 493                      $nextid = self::get_next_sequence_starting_value($records, $table);
 494                      $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
 495                  }
 496              }
 497              if ($queries) {
 498                  $DB->change_database_structure(implode(';', $queries));
 499              }
 500  
 501          } else if ($dbfamily === 'mysql') {
 502              $queries = array();
 503              $sequences = array();
 504              $prefix = $DB->get_prefix();
 505              $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
 506              foreach ($rs as $info) {
 507                  $table = strtolower($info->name);
 508                  if (strpos($table, $prefix) !== 0) {
 509                      // incorrect table match caused by _
 510                      continue;
 511                  }
 512                  if (!is_null($info->auto_increment)) {
 513                      $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
 514                      $sequences[$table] = $info->auto_increment;
 515                  }
 516              }
 517              $rs->close();
 518              $prefix = $DB->get_prefix();
 519              foreach ($data as $table => $records) {
 520                  // If table is not modified then no need to do anything.
 521                  if (!isset($updatedtables[$table])) {
 522                      continue;
 523                  }
 524                  if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 525                      if (isset($sequences[$table])) {
 526                          $nextid = self::get_next_sequence_starting_value($records, $table);
 527                          if ($sequences[$table] != $nextid) {
 528                              $queries[] = "ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid";
 529                          }
 530                      } else {
 531                          // some problem exists, fallback to standard code
 532                          $DB->get_manager()->reset_sequence($table);
 533                      }
 534                  }
 535              }
 536              if ($queries) {
 537                  $DB->change_database_structure(implode(';', $queries));
 538              }
 539  
 540          } else if ($dbfamily === 'oracle') {
 541              $sequences = self::get_sequencenames();
 542              $sequences = array_map('strtoupper', $sequences);
 543              $lookup = array_flip($sequences);
 544  
 545              $current = array();
 546              list($seqs, $params) = $DB->get_in_or_equal($sequences);
 547              $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
 548              $rs = $DB->get_recordset_sql($sql, $params);
 549              foreach ($rs as $seq) {
 550                  $table = $lookup[$seq->sequence_name];
 551                  $current[$table] = $seq->last_number;
 552              }
 553              $rs->close();
 554  
 555              foreach ($data as $table => $records) {
 556                  // If table is not modified then no need to do anything.
 557                  if (!isset($updatedtables[$table])) {
 558                      continue;
 559                  }
 560                  if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 561                      $lastrecord = end($records);
 562                      if ($lastrecord) {
 563                          $nextid = $lastrecord->id + 1;
 564                      } else {
 565                          $nextid = 1;
 566                      }
 567                      if (!isset($current[$table])) {
 568                          $DB->get_manager()->reset_sequence($table);
 569                      } else if ($nextid == $current[$table]) {
 570                          continue;
 571                      }
 572                      // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
 573                      $seqname = $sequences[$table];
 574                      $cachesize = $DB->get_manager()->generator->sequence_cache_size;
 575                      $DB->change_database_structure("DROP SEQUENCE $seqname");
 576                      $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
 577                  }
 578              }
 579  
 580          } else {
 581              // note: does mssql support any kind of faster reset?
 582              // This also implies mssql will not use unique sequence values.
 583              if (is_null($empties) and (empty($updatedtables))) {
 584                  $empties = self::guess_unmodified_empty_tables();
 585              }
 586              foreach ($data as $table => $records) {
 587                  // If table is not modified then no need to do anything.
 588                  if (isset($empties[$table]) or (!isset($updatedtables[$table]))) {
 589                      continue;
 590                  }
 591                  if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 592                      $DB->get_manager()->reset_sequence($table);
 593                  }
 594              }
 595          }
 596      }
 597  
 598      /**
 599       * Reset all database tables to default values.
 600       * @static
 601       * @return bool true if reset done, false if skipped
 602       */
 603      public static function reset_database() {
 604          global $DB;
 605  
 606          $tables = $DB->get_tables(false);
 607          if (!$tables or empty($tables['config'])) {
 608              // not installed yet
 609              return false;
 610          }
 611  
 612          if (!$data = self::get_tabledata()) {
 613              // not initialised yet
 614              return false;
 615          }
 616          if (!$structure = self::get_tablestructure()) {
 617              // not initialised yet
 618              return false;
 619          }
 620  
 621          $empties = array();
 622          // Use local copy of self::$tableupdated, as list gets updated in for loop.
 623          $updatedtables = self::$tableupdated;
 624  
 625          // If empty tablesequences list then it's the very first run.
 626          if (empty(self::$tablesequences) && (($DB->get_dbfamily() != 'mysql') && ($DB->get_dbfamily() != 'postgres'))) {
 627              // Only Mysql and Postgres support random sequence, so don't guess, just reset everything on very first run.
 628              $empties = self::guess_unmodified_empty_tables();
 629          }
 630  
 631          // Check if any table has been modified by behat selenium process.
 632          if (defined('BEHAT_SITE_RUNNING')) {
 633              // Crazy way to reset :(.
 634              $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
 635              if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
 636                  self::$tableupdated = array_merge(self::$tableupdated, $tablesupdated);
 637                  unlink($tablesupdatedfile);
 638              }
 639              $updatedtables = self::$tableupdated;
 640          }
 641  
 642          $borkedmysql = false;
 643          if ($DB->get_dbfamily() === 'mysql') {
 644              $version = $DB->get_server_info();
 645              if (version_compare($version['version'], '5.6.0') == 1 and version_compare($version['version'], '5.6.16') == -1) {
 646                  // Everything that comes from Oracle is evil!
 647                  //
 648                  // See http://dev.mysql.com/doc/refman/5.6/en/alter-table.html
 649                  // You cannot reset the counter to a value less than or equal to to the value that is currently in use.
 650                  //
 651                  // From 5.6.16 release notes:
 652                  //   InnoDB: The ALTER TABLE INPLACE algorithm would fail to decrease the auto-increment value.
 653                  //           (Bug #17250787, Bug #69882)
 654                  $borkedmysql = true;
 655  
 656              } else if (version_compare($version['version'], '10.0.0') == 1) {
 657                  // And MariaDB is no better!
 658                  // Let's hope they pick the patch sometime later...
 659                  $borkedmysql = true;
 660              }
 661          }
 662  
 663          if ($borkedmysql) {
 664              $mysqlsequences = array();
 665              $prefix = $DB->get_prefix();
 666              $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
 667              foreach ($rs as $info) {
 668                  $table = strtolower($info->name);
 669                  if (strpos($table, $prefix) !== 0) {
 670                      // Incorrect table match caused by _ char.
 671                      continue;
 672                  }
 673                  if (!is_null($info->auto_increment)) {
 674                      $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
 675                      $mysqlsequences[$table] = $info->auto_increment;
 676                  }
 677              }
 678          }
 679  
 680          foreach ($data as $table => $records) {
 681              // If table is not modified then no need to do anything.
 682              // $updatedtables tables is set after the first run, so check before checking for specific table update.
 683              if (!empty($updatedtables) && !isset($updatedtables[$table])) {
 684                  continue;
 685              }
 686  
 687              if ($borkedmysql) {
 688                  if (empty($records)) {
 689                      if (!isset($empties[$table])) {
 690                          // Table has been modified and is not empty.
 691                          $DB->delete_records($table, null);
 692                      }
 693                      continue;
 694                  }
 695  
 696                  if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 697                      $current = $DB->get_records($table, array(), 'id ASC');
 698                      if ($current == $records) {
 699                          if (isset($mysqlsequences[$table]) and $mysqlsequences[$table] == $structure[$table]['id']->auto_increment) {
 700                              continue;
 701                          }
 702                      }
 703                  }
 704  
 705                  // Use TRUNCATE as a workaround and reinsert everything.
 706                  $DB->delete_records($table, null);
 707                  foreach ($records as $record) {
 708                      $DB->import_record($table, $record, false, true);
 709                  }
 710                  continue;
 711              }
 712  
 713              if (empty($records)) {
 714                  if (!isset($empties[$table])) {
 715                      // Table has been modified and is not empty.
 716                      $DB->delete_records($table, array());
 717                  }
 718                  continue;
 719              }
 720  
 721              if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
 722                  $currentrecords = $DB->get_records($table, array(), 'id ASC');
 723                  $changed = false;
 724                  foreach ($records as $id => $record) {
 725                      if (!isset($currentrecords[$id])) {
 726                          $changed = true;
 727                          break;
 728                      }
 729                      if ((array)$record != (array)$currentrecords[$id]) {
 730                          $changed = true;
 731                          break;
 732                      }
 733                      unset($currentrecords[$id]);
 734                  }
 735                  if (!$changed) {
 736                      if ($currentrecords) {
 737                          $lastrecord = end($records);
 738                          $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
 739                          continue;
 740                      } else {
 741                          continue;
 742                      }
 743                  }
 744              }
 745  
 746              $DB->delete_records($table, array());
 747              foreach ($records as $record) {
 748                  $DB->import_record($table, $record, false, true);
 749              }
 750          }
 751  
 752          // reset all next record ids - aka sequences
 753          self::reset_all_database_sequences($empties);
 754  
 755          // remove extra tables
 756          foreach ($tables as $table) {
 757              if (!isset($data[$table])) {
 758                  $DB->get_manager()->drop_table(new xmldb_table($table));
 759              }
 760          }
 761  
 762          self::reset_updated_table_list();
 763  
 764          return true;
 765      }
 766  
 767      /**
 768       * Purge dataroot directory
 769       * @static
 770       * @return void
 771       */
 772      public static function reset_dataroot() {
 773          global $CFG;
 774  
 775          $childclassname = self::get_framework() . '_util';
 776  
 777          // Do not delete automatically installed files.
 778          self::skip_original_data_files($childclassname);
 779  
 780          // Clear file status cache, before checking file_exists.
 781          clearstatcache();
 782  
 783          // Clean up the dataroot folder.
 784          $handle = opendir(self::get_dataroot());
 785          while (false !== ($item = readdir($handle))) {
 786              if (in_array($item, $childclassname::$datarootskiponreset)) {
 787                  continue;
 788              }
 789              if (is_dir(self::get_dataroot()."/$item")) {
 790                  remove_dir(self::get_dataroot()."/$item", false);
 791              } else {
 792                  unlink(self::get_dataroot()."/$item");
 793              }
 794          }
 795          closedir($handle);
 796  
 797          // Clean up the dataroot/filedir folder.
 798          if (file_exists(self::get_dataroot() . '/filedir')) {
 799              $handle = opendir(self::get_dataroot() . '/filedir');
 800              while (false !== ($item = readdir($handle))) {
 801                  if (in_array('filedir' . DIRECTORY_SEPARATOR . $item, $childclassname::$datarootskiponreset)) {
 802                      continue;
 803                  }
 804                  if (is_dir(self::get_dataroot()."/filedir/$item")) {
 805                      remove_dir(self::get_dataroot()."/filedir/$item", false);
 806                  } else {
 807                      unlink(self::get_dataroot()."/filedir/$item");
 808                  }
 809              }
 810              closedir($handle);
 811          }
 812  
 813          make_temp_directory('');
 814          make_cache_directory('');
 815          make_localcache_directory('');
 816          // Reset the cache API so that it recreates it's required directories as well.
 817          cache_factory::reset();
 818          // Purge all data from the caches. This is required for consistency.
 819          // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
 820          // and now we will purge any other caches as well.
 821          cache_helper::purge_all();
 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                  if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
 926                      $tablesupdated[$table] = true;
 927                  } else {
 928                      $tablesupdated[$table] = true;
 929                  }
 930                  @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT));
 931              }
 932          }
 933      }
 934  
 935      /**
 936       * Reset updated table list. This should be done after every reset.
 937       */
 938      public static function reset_updated_table_list() {
 939          self::$tableupdated = array();
 940      }
 941  
 942      /**
 943       * Returns the path to the file which holds list of tables updated in scenario.
 944       * @return string
 945       */
 946      protected final static function get_tables_updated_by_scenario_list_path() {
 947          return self::get_dataroot() . '/tablesupdatedbyscenario.txt';
 948      }
 949  
 950      /**
 951       * Drop the whole test database
 952       * @static
 953       * @param bool $displayprogress
 954       */
 955      protected static function drop_database($displayprogress = false) {
 956          global $DB;
 957  
 958          $tables = $DB->get_tables(false);
 959          if (isset($tables['config'])) {
 960              // config always last to prevent problems with interrupted drops!
 961              unset($tables['config']);
 962              $tables['config'] = 'config';
 963          }
 964  
 965          if ($displayprogress) {
 966              echo "Dropping tables:\n";
 967          }
 968          $dotsonline = 0;
 969          foreach ($tables as $tablename) {
 970              $table = new xmldb_table($tablename);
 971              $DB->get_manager()->drop_table($table);
 972  
 973              if ($dotsonline == 60) {
 974                  if ($displayprogress) {
 975                      echo "\n";
 976                  }
 977                  $dotsonline = 0;
 978              }
 979              if ($displayprogress) {
 980                  echo '.';
 981              }
 982              $dotsonline += 1;
 983          }
 984          if ($displayprogress) {
 985              echo "\n";
 986          }
 987      }
 988  
 989      /**
 990       * Drops the test framework dataroot
 991       * @static
 992       */
 993      protected static function drop_dataroot() {
 994          global $CFG;
 995  
 996          $framework = self::get_framework();
 997          $childclassname = $framework . '_util';
 998  
 999          $files = scandir(self::get_dataroot() . '/'  . $framework);
1000          foreach ($files as $file) {
1001              if (in_array($file, $childclassname::$datarootskipondrop)) {
1002                  continue;
1003              }
1004              $path = self::get_dataroot() . '/' . $framework . '/' . $file;
1005              if (is_dir($path)) {
1006                  remove_dir($path, false);
1007              } else {
1008                  unlink($path);
1009              }
1010          }
1011  
1012          $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1013          if (file_exists($jsonfilepath)) {
1014              // Delete the json file.
1015              unlink($jsonfilepath);
1016              // Delete the dataroot filedir.
1017              remove_dir(self::get_dataroot() . '/filedir', false);
1018          }
1019      }
1020  
1021      /**
1022       * Skip the original dataroot files to not been reset.
1023       *
1024       * @static
1025       * @param string $utilclassname the util class name..
1026       */
1027      protected static function skip_original_data_files($utilclassname) {
1028          $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1029          if (file_exists($jsonfilepath)) {
1030  
1031              $listfiles = file_get_contents($jsonfilepath);
1032  
1033              // Mark each files as to not be reset.
1034              if (!empty($listfiles) && !self::$originaldatafilesjsonadded) {
1035                  $originaldatarootfiles = json_decode($listfiles);
1036                  // Keep the json file. Only drop_dataroot() should delete it.
1037                  $originaldatarootfiles[] = self::$originaldatafilesjson;
1038                  $utilclassname::$datarootskiponreset = array_merge($utilclassname::$datarootskiponreset,
1039                      $originaldatarootfiles);
1040                  self::$originaldatafilesjsonadded = true;
1041              }
1042          }
1043      }
1044  
1045      /**
1046       * Save the list of the original dataroot files into a json file.
1047       */
1048      protected static function save_original_data_files() {
1049          global $CFG;
1050  
1051          $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1052  
1053          // Save the original dataroot files if not done (only executed the first time).
1054          if (!file_exists($jsonfilepath)) {
1055  
1056              $listfiles = array();
1057              $currentdir = 'filedir' . DIRECTORY_SEPARATOR . '.';
1058              $parentdir = 'filedir' . DIRECTORY_SEPARATOR . '..';
1059              $listfiles[$currentdir] = $currentdir;
1060              $listfiles[$parentdir] = $parentdir;
1061  
1062              $filedir = self::get_dataroot() . '/filedir';
1063              if (file_exists($filedir)) {
1064                  $directory = new RecursiveDirectoryIterator($filedir);
1065                  foreach (new RecursiveIteratorIterator($directory) as $file) {
1066                      if ($file->isDir()) {
1067                          $key = substr($file->getPath(), strlen(self::get_dataroot() . '/'));
1068                      } else {
1069                          $key = substr($file->getPathName(), strlen(self::get_dataroot() . '/'));
1070                      }
1071                      $listfiles[$key] = $key;
1072                  }
1073              }
1074  
1075              // Save the file list in a JSON file.
1076              $fp = fopen($jsonfilepath, 'w');
1077              fwrite($fp, json_encode(array_values($listfiles)));
1078              fclose($fp);
1079          }
1080      }
1081  
1082      /**
1083       * Return list of environment versions on which tests will run.
1084       * Environment includes:
1085       * - moodleversion
1086       * - phpversion
1087       * - dbtype
1088       * - dbversion
1089       * - os
1090       *
1091       * @return array
1092       */
1093      public static function get_environment() {
1094          global $CFG, $DB;
1095  
1096          $env = array();
1097  
1098          // Add moodle version.
1099          $release = null;
1100          require("$CFG->dirroot/version.php");
1101          $env['moodleversion'] = $release;
1102  
1103          // Add php version.
1104          $phpversion = phpversion();
1105          $env['phpversion'] = $phpversion;
1106  
1107          // Add database type and version.
1108          $dbtype = $CFG->dbtype;
1109          $dbinfo = $DB->get_server_info();
1110          $dbversion = $dbinfo['version'];
1111          $env['dbtype'] = $dbtype;
1112          $env['dbversion'] = $dbversion;
1113  
1114          // OS details.
1115          $osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m');
1116          $env['os'] = $osdetails;
1117  
1118          return $env;
1119      }
1120  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1