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