[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |